summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--RELEASE-NOTES-1.2625
-rw-r--r--extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php29
-rw-r--r--extensions/SyntaxHighlight_GeSHi/composer.json2
-rw-r--r--extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php22
-rw-r--r--extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php23
-rw-r--r--includes/DefaultSettings.php8
-rw-r--r--includes/Hooks.php42
-rw-r--r--includes/HttpFunctions.php17
-rw-r--r--includes/MediaWiki.php44
-rw-r--r--includes/Setup.php15
-rw-r--r--includes/User.php7
-rw-r--r--includes/debug/logger/LoggerFactory.php2
-rw-r--r--includes/libs/MultiHttpClient.php13
-rw-r--r--includes/libs/objectcache/APCBagOStuff.php54
-rw-r--r--includes/specialpage/RedirectSpecialPage.php12
-rw-r--r--includes/specials/SpecialExpandTemplates.php2
-rw-r--r--includes/specials/SpecialMyLanguage.php11
-rw-r--r--includes/specials/SpecialMyRedirectPages.php50
-rw-r--r--includes/specials/SpecialSearch.php2
-rw-r--r--includes/utils/IP.php22
-rw-r--r--tests/phpunit/includes/parser/MediaWikiParserTest.php1
-rw-r--r--tests/phpunit/includes/parser/NewParserTest.php1
-rw-r--r--tests/phpunit/includes/utils/IPTest.php33
-rw-r--r--tests/phpunit/suite.xml4
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>