diff options
24 files changed, 356 insertions, 85 deletions
diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index 81405f50..0adfbe20 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -1,6 +1,31 @@ Security reminder: If you have PHP's register_globals option set, you must turn it off. MediaWiki will not work with it enabled. +== MediaWiki 1.26.1 == + +This is a maintenance release of the MediaWiki 1.26 branch. + +=== Changes since 1.26.0 === +* (T117899) SECURITY: $wgArticlePath can no longer be set to relative paths + that do not begin with a slash. This enabled trivial XSS attacks. + Configuration values such as "http://my.wiki.com/wiki/$1" are fine, as are + "/wiki/$1". A value such as "$1" or "wiki/$1" is not and will now throw an + error. +* (T119309) SECURITY: Use hash_equals() for edit token comparison +* (T118032) SECURITY: Don't allow cURL to interpret POST parameters starting + with '@' as file uploads +* (T115522) SECURITY: Passwords generated by User::randomPassword() can no + longer be shorter than $wgMinimalPasswordLength +* (T97897) SECURITY: Improve IP parsing and trimming. Previous behavior could + result in improper blocks being issued +* (T109724) SECURITY: Special:MyPage, Special:MyTalk, Special:MyContributions + and related pages no longer use HTTP redirects and are now redirected by + MediaWiki +* Fixed ConfigException in ExpandTemplates due to AlwaysUseTidy. +* Fixed stray literal \n in Special:Search. +* Fix issue that breaks HHVM Repo Authorative mode. +* (T120267) Work around APCu memory corruption bug + == MediaWiki 1.26 == === Configuration changes in 1.26 === diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php index 535e37af..9eed2763 100644 --- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php +++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php @@ -16,7 +16,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; // @codingStandardsIgnoreStart class SyntaxHighlight_GeSHi { @@ -276,18 +276,29 @@ class SyntaxHighlight_GeSHi { $output = $cache->get( $cacheKey ); if ( $output === false ) { - try { - $pygments = new Pygments( $wgPygmentizePath ); - $output = $pygments->highlight( $code, $lexer, 'html', $options ); - } catch ( RuntimeException $e ) { + $optionPairs = array(); + foreach ( $options as $k => $v ) { + $optionPairs[] = "{$k}={$v}"; + } + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + $process = $builder + ->add( '-l' )->add( $lexer ) + ->add( '-f' )->add( 'html' ) + ->add( '-O' )->add( implode( ',', $optionPairs ) ) + ->getProcess(); + + $process->setInput( $code ); + $process->run(); + + if ( !$process->isSuccessful() ) { $status->warning( 'syntaxhighlight-error-pygments-invocation-failure' ); - wfWarn( - 'Failed to invoke Pygments. Please check that Pygments is installed ' . - 'and that $wgPygmentizePath is accurate.' - ); + wfWarn( 'Failed to invoke Pygments: ' . $process->getErrorOutput() ); $status->value = self::highlight( $code, null, $args )->getValue(); return $status; } + + $output = $process->getOutput(); $cache->set( $cacheKey, $output ); } diff --git a/extensions/SyntaxHighlight_GeSHi/composer.json b/extensions/SyntaxHighlight_GeSHi/composer.json index 709c1fb0..d8b8cc8e 100644 --- a/extensions/SyntaxHighlight_GeSHi/composer.json +++ b/extensions/SyntaxHighlight_GeSHi/composer.json @@ -2,7 +2,7 @@ "name": "mediawiki/syntax-highlight_geshi", "description": "Syntax highlighting extension for MediaWiki", "require": { - "kzykhys/pygments": "1.0" + "symfony/process": "~2.5" }, "require-dev": { "jakub-onderka/php-parallel-lint": "0.9", diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php index a3c0c817..9299cd74 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -39,9 +39,25 @@ class UpdateCSS extends Maintenance { global $wgPygmentizePath; $target = __DIR__ . '/../modules/pygments.generated.css'; - $pygments = new Pygments( $wgPygmentizePath ); $css = "/* Stylesheet generated by updateCSS.php */\n"; - $css .= $pygments->getCss( 'default', '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder + ->add( '-f' )->add( 'html' ) + ->add( '-S' )->add( 'default' ) + ->add( '-a' )->add( '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ) + ->getProcess(); + + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $css .= $process->getOutput(); + if ( file_put_contents( $target, $css ) === false ) { $this->output( "Failed to write to {$target}\n" ); } else { diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php index 75beb9b5..b5a7fc5a 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -43,8 +43,25 @@ class UpdateLanguageList extends Maintenance { $header = '// Generated by ' . basename( __FILE__ ) . "\n\n"; - $pygments = new Pygments( $wgPygmentizePath ); - $lexers = array_keys( $pygments->getLexers() ); + $lexers = array(); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder->add( '-L' )->add( 'lexer' )->getProcess(); + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $output = $process->getOutput(); + foreach ( explode( "\n", $output ) as $line ) { + if ( substr( $line, 0, 1 ) === '*' ) { + $newLexers = explode( ', ', trim( $line, "* :\n" ) ); + $lexers = array_merge( $lexers, $newLexers ); + } + } sort( $lexers ); $code = "<?php\n" . $header . 'return ' . var_export( $lexers, true ) . ";\n"; diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 268a8d19..919d05b8 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,7 +75,7 @@ $wgConfigRegistry = array( * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.26.0'; +$wgVersion = '1.26.1'; /** * Name of the site. It must be changed in LocalSettings.php @@ -4680,6 +4680,12 @@ $wgWhitelistReadRegexp = false; $wgEmailConfirmToEdit = false; /** + * Should MediaWiki attempt to protect user's privacy when doing redirects? + * Keep this true if access counts to articles are made public. + */ +$wgHideIdentifiableRedirects = true; + +/** * Permission keys given to users in each group. * * This is an array where the keys are all groups and each value is an diff --git a/includes/Hooks.php b/includes/Hooks.php index a4145624..90185816 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -193,34 +193,17 @@ class Hooks { $badhookmsg = null; $hook_args = array_merge( $hook, $args ); - set_error_handler( 'Hooks::hookErrorHandler' ); - // mark hook as deprecated, if deprecation version is specified if ( $deprecatedVersion !== null ) { wfDeprecated( "$event hook (used in $func)", $deprecatedVersion ); } - try { - $retval = call_user_func_array( $callback, $hook_args ); - } catch ( MWHookException $e ) { - $badhookmsg = $e->getMessage(); - } catch ( Exception $e ) { - restore_error_handler(); - throw $e; - } - - restore_error_handler(); + $retval = call_user_func_array( $callback, $hook_args ); // Process the return value. if ( is_string( $retval ) ) { // String returned means error. throw new FatalError( $retval ); - } elseif ( $badhookmsg !== null ) { - // Exception was thrown from Hooks::hookErrorHandler. - throw new MWException( - 'Detected bug in an extension! ' . - "Hook $func has invalid call signature; " . $badhookmsg - ); } elseif ( $retval === false ) { // False was returned. Stop processing, but no error. return false; @@ -229,27 +212,4 @@ class Hooks { return true; } - - /** - * 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 - */ - public static function hookErrorHandler( $errno, $errstr ) { - if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { - throw new MWHookException( $errstr, $errno ); - } - - // Delegate unhandled errors to the default MW handler - return call_user_func_array( - 'MWExceptionHandler::handleError', func_get_args() - ); - } } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index bc5a9570..3dff9711 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -779,7 +779,22 @@ class CurlHttpRequest extends MWHttpRequest { $this->curlOptions[CURLOPT_HEADER] = true; } elseif ( $this->method == 'POST' ) { $this->curlOptions[CURLOPT_POST] = true; - $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; + $postData = $this->postData; + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true; + } else if ( is_array( $postData ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $postData = wfArrayToCgi( $postData ); + } + $this->curlOptions[CURLOPT_POSTFIELDS] = $postData; + // Suppress 'Expect: 100-continue' header, as some servers // will reject it with a 417 and Curl won't auto retry // with HTTP 1.0 fallback diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index fbacb250..2da2f6ce 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -37,6 +37,11 @@ class MediaWiki { private $config; /** + * @var String Cache what action this request is + */ + private $action; + + /** * @param IContextSource|null $context */ public function __construct( IContextSource $context = null ) { @@ -141,13 +146,11 @@ class MediaWiki { * @return string Action */ public function getAction() { - static $action = null; - - if ( $action === null ) { - $action = Action::getActionName( $this->context ); + if ( $this->action === null ) { + $this->action = Action::getActionName( $this->context ); } - return $action; + return $this->action; } /** @@ -242,8 +245,37 @@ class MediaWiki { // Handle any other redirects. // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + // Prevent information leak via Special:MyPage et al (T109724) + if ( $title->isSpecialPage() ) { + $specialPage = SpecialPageFactory::getPage( $title->getDBKey() ); + if ( $specialPage instanceof RedirectSpecialPage + && $this->config->get( 'HideIdentifiableRedirects' ) + && $specialPage->personallyIdentifiableTarget() + ) { + list( , $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBKey() ); + $target = $specialPage->getRedirect( $subpage ); + // target can also be true. We let that case fall through to normal processing. + if ( $target instanceof Title ) { + $query = $specialPage->getRedirectQuery() ?: array(); + $request = new DerivativeRequest( $this->context->getRequest(), $query ); + $request->setRequestURL( $this->context->getRequest()->getRequestURL() ); + $this->context->setRequest( $request ); + // Do not varnish cache these. May vary even for anons + $this->context->getOutput()->lowerCdnMaxage( 0 ); + $this->context->setTitle( $target ); + $wgTitle = $target; + // Reset action type cache. (Special pages have only view) + $this->action = null; + $title = $target; + $output->addJsConfigVars( array( + 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ), + ) ); + $output->addModules( 'mediawiki.action.view.redirect' ); + } + } + } - // Special pages + // Special pages ($title may have changed since if statement above) if ( NS_SPECIAL == $title->getNamespace() ) { // Actions that need to be made when we have a special pages SpecialPageFactory::executePath( $title, $this->context ); diff --git a/includes/Setup.php b/includes/Setup.php index 70e8cde4..905a1d10 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -510,6 +510,21 @@ MWExceptionHandler::installHandler(); require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; + +$ps_validation = Profiler::instance()->scopedProfileIn( $fname . '-validation' ); + +// T48998: Bail out early if $wgArticlePath is non-absolute +if ( !preg_match( '/^(https?:\/\/|\/)/', $wgArticlePath ) ) { + throw new FatalError( + 'If you use a relative URL for $wgArticlePath, it must start ' . + 'with a slash (<code>/</code>).<br><br>See ' . + '<a href="https://www.mediawiki.org/wiki/Manual:$wgArticlePath">' . + 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath</a>.' + ); +} + +Profiler::instance()->scopedProfileOut( $ps_validation ); + $ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); if ( $wgScriptExtension !== '.php' || defined( 'MW_ENTRY_PHP5' ) ) { diff --git a/includes/User.php b/includes/User.php index 22c90cdd..199dd1dc 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1029,11 +1029,10 @@ class User implements IDBAccessObject { // stopping at a minimum of 10 chars. $length = max( 10, $wgMinimalPasswordLength ); // Multiply by 1.25 to get the number of hex characters we need - $length = $length * 1.25; // Generate random hex chars - $hex = MWCryptRand::generateHex( $length ); + $hex = MWCryptRand::generateHex( ceil( $length * 1.25 ) ); // Convert from base 16 to base 32 to get a proper password like string - return wfBaseConvert( $hex, 16, 32 ); + return substr( wfBaseConvert( $hex, 16, 32, $length ), -$length ); } /** @@ -4177,7 +4176,7 @@ class User implements IDBAccessObject { $salt, $request ?: $this->getRequest(), $timestamp ); - if ( $val != $sessionToken ) { + if ( !hash_equals( $sessionToken, $val ) ) { wfDebug( "User::matchEditToken: broken session data\n" ); } diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php index 0b6965ff..1e44b708 100644 --- a/includes/debug/logger/LoggerFactory.php +++ b/includes/debug/logger/LoggerFactory.php @@ -94,7 +94,7 @@ class LoggerFactory { * @return \\Psr\\Log\\LoggerInterface */ public static function getInstance( $channel ) { - if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { $message = ( 'MediaWiki requires the <a href="https://github.com/php-fig/log">PSR-3 logging ' . "library</a> to be present. This library is not embedded directly in MediaWiki's " . diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 6af3ed51..5555cbcb 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -335,6 +335,19 @@ class MultiHttpClient { ); } elseif ( $req['method'] === 'POST' ) { curl_setopt( $ch, CURLOPT_POST, 1 ); + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true ); + } else if ( is_array( $req['body'] ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $req['body'] = wfArrayToCgi( $req['body'] ); + } curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] ); } else { if ( is_resource( $req['body'] ) || $req['body'] !== '' ) { diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0dbbaba9..35e05e80 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -27,22 +27,72 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + + /** + * @var bool If true, trust the APC implementation to serialize and + * deserialize objects correctly. If false, (de-)serialize in PHP. + */ + protected $nativeSerialize; + /** * @var string String to append to each APC key. This may be changed * whenever the handling of values is changed, to prevent existing code * from encountering older values which it cannot handle. - **/ - const KEY_SUFFIX = ':1'; + */ + const KEY_SUFFIX = ':2'; + + /** + * Constructor + * + * Available parameters are: + * - nativeSerialize: If true, pass objects to apc_store(), and trust it + * to serialize them correctly. If false, serialize + * all values in PHP. + * + * @param array $params + */ + public function __construct( array $params = array() ) { + parent::__construct( $params ); + + if ( isset( $params['nativeSerialize'] ) ) { + $this->nativeSerialize = $params['nativeSerialize']; + } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) { + // APCu has a memory corruption bug when the serializer is set to 'default'. + // See T120267, and upstream bug reports: + // - https://github.com/krakjoe/apcu/issues/38 + // - https://github.com/krakjoe/apcu/issues/35 + // - https://github.com/krakjoe/apcu/issues/111 + $this->logger->warning( + 'The APCu extension is loaded and the apc.serializer INI setting ' . + 'is set to "default". This can cause memory corruption! ' . + 'You should change apc.serializer to "php" instead. ' . + 'See <https://github.com/krakjoe/apcu/issues/38>.' + ); + $this->nativeSerialize = false; + } else { + $this->nativeSerialize = true; + } + } public function get( $key, &$casToken = null, $flags = 0 ) { $val = apc_fetch( $key . self::KEY_SUFFIX ); $casToken = $val; + if ( is_string( $val ) && !$this->nativeSerialize ) { + $val = $this->isInteger( $val ) + ? intval( $val ) + : unserialize( $val ); + } + return $val; } public function set( $key, $value, $exptime = 0 ) { + if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + apc_store( $key . self::KEY_SUFFIX, $value, $exptime ); return true; diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index 9129ee5d..5047354e 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -94,6 +94,18 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { ? $params : false; } + + /** + * Indicate if the target of this redirect can be used to identify + * a particular user of this wiki (e.g., if the redirect is to the + * user page of a User). See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return false; + } } /** diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index b7582e6c..06eb2769 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage { } $config = $this->getConfig(); - if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + if ( $config->get( 'UseTidy' ) && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); } diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 3d8ff97b..d11fbe63 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -99,4 +99,15 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return $base; } } + + /** + * Target can identify a specific user's language preference. + * + * @see T109724 + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php index 5ef03f13..850b1f63 100644 --- a/includes/specials/SpecialMyRedirectPages.php +++ b/includes/specials/SpecialMyRedirectPages.php @@ -45,6 +45,16 @@ class SpecialMypage extends RedirectSpecialArticle { return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -68,6 +78,16 @@ class SpecialMytalk extends RedirectSpecialArticle { return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -90,6 +110,16 @@ class SpecialMycontributions extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -110,6 +140,16 @@ class SpecialMyuploads extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -132,4 +172,14 @@ class SpecialAllMyUploads extends RedirectSpecialPage { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index f50fb732..8c546edd 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -388,7 +388,7 @@ class SpecialSearch extends SpecialPage { } } - $out->addHTML( '<div class="visualClear"></div>\n' ); + $out->addHTML( '<div class="visualClear"></div>' ); if ( $prevnext ) { $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); } diff --git a/includes/utils/IP.php b/includes/utils/IP.php index 666660aa..13586f3c 100644 --- a/includes/utils/IP.php +++ b/includes/utils/IP.php @@ -132,8 +132,9 @@ class IP { /** * Convert an IP into a verbose, uppercase, normalized form. - * IPv6 addresses in octet notation are expanded to 8 words. - * IPv4 addresses are just trimmed. + * Both IPv4 and IPv6 addresses are trimmed. Additionally, + * IPv6 addresses in octet notation are expanded to 8 words; + * IPv4 addresses have leading zeros, in each octet, removed. * * @param string $ip IP address in quad or octet form (CIDR or not). * @return string @@ -143,8 +144,16 @@ class IP { if ( $ip === '' ) { return null; } - if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) { - return $ip; // nothing else to do for IPv4 addresses or invalid ones + /* If not an IP, just return trimmed value, since sanitizeIP() is called + * in a number of contexts where usernames are supplied as input. + */ + if ( !self::isIPAddress($ip) ) { + return $ip; + } + if ( self::isIPv4( $ip ) ) { + // Remove leading 0's from octet representation of IPv4 address + $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip ); + return $ip; } // Remove any whitespaces, convert to upper case $ip = strtoupper( $ip ); @@ -399,8 +408,9 @@ class IP { if ( self::isIPv6( $ip ) ) { $n = 'v6-' . self::IPv6ToRawHex( $ip ); } elseif ( self::isIPv4( $ip ) ) { - // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08) - $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip ); + // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08), + // also double/triple 0 needs to be changed to just a single 0 for ip2long. + $ip = self::sanitizeIP( $ip ); $n = ip2long( $ip ); if ( $n < 0 ) { $n += pow( 2, 32 ); diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index 96ae3bec..210c17c5 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -7,6 +7,7 @@ require_once __DIR__ . '/NewParserTest.php'; * an PHPUnit_Framework_Test object * * @group Parser + * @group ParserTests * @group Database */ class MediaWikiParserTest { diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index df7da98c..d95e9225 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -672,6 +672,7 @@ class NewParserTest extends MediaWikiTestCase { /** * @group medium + * @group ParserTests * @dataProvider parserTestProvider * @param string $desc * @param string $input diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php index 04b8f486..34aff796 100644 --- a/tests/phpunit/includes/utils/IPTest.php +++ b/tests/phpunit/includes/utils/IPTest.php @@ -307,12 +307,34 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Improve IP::sanitizeIP() code coverage - * @todo Most probably incomplete + * @covers IP::sanitizeIP + * @dataProvider provideSanitizeIP */ - public function testSanitizeIP() { - $this->assertNull( IP::sanitizeIP( '' ) ); - $this->assertNull( IP::sanitizeIP( ' ' ) ); + public function testSanitizeIP( $expected, $input ) { + $result = IP::sanitizeIP( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testSanitizeIP() + */ + public static function provideSanitizeIP() { + return array( + array( '0.0.0.0', '0.0.0.0' ), + array( '0.0.0.0', '00.00.00.00' ), + array( '0.0.0.0', '000.000.000.000' ), + array( '141.0.11.253', '141.000.011.253' ), + array( '1.2.4.5', '1.2.4.5' ), + array( '1.2.4.5', '01.02.04.05' ), + array( '1.2.4.5', '001.002.004.005' ), + array( '10.0.0.1', '010.0.000.1' ), + array( '80.72.250.4', '080.072.250.04' ), + array( 'Foo.1000.00', 'Foo.1000.00'), + array( 'Bar.01', 'Bar.01'), + array( 'Bar.010', 'Bar.010'), + array( null, ''), + array( null, ' ') + ); } /** @@ -336,6 +358,7 @@ class IPTest extends PHPUnit_Framework_TestCase { array( '80000000', '128.0.0.0' ), array( 'DEADCAFE', '222.173.202.254' ), array( 'FFFFFFFF', '255.255.255.255' ), + array( '8D000BFD', '141.000.11.253' ), array( false, 'IN.VA.LI.D' ), array( 'v6-00000000000000000000000000000001', '::1' ), array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ), diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index 1acbc241..82086b9d 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -24,6 +24,10 @@ phpunit.php enables colors for other OSs at runtime <testsuite name="languages"> <directory>languages</directory> </testsuite> + <testsuite name="parsertests"> + <file>includes/parser/MediaWikiParserTest.php</file> + <file>suites/ExtensionsParserTestSuite.php</file> + </testsuite> <testsuite name="skins"> <directory>skins</directory> </testsuite> |