Update code for voice and tone reasons

* Reduces use of whitelist
* Deprecates and provides new $wg to replace $wgCaptchaWhitelistIP and $wgCaptchaWhitelist

Bug: T277936
Change-Id: I9c4a572321bb06e5a1d4208a295e21b73e75b787
This commit is contained in:
Reedy 2024-11-06 23:56:11 +00:00
parent 890370b888
commit da22e2c767
5 changed files with 71 additions and 56 deletions

View file

@ -45,9 +45,9 @@ Additional maintenance work was done by Yaron Koren.
* *
* Specific IP addresses or CIDR-style ranges may be used, * Specific IP addresses or CIDR-style ranges may be used,
* for instance: * for instance:
* $wgCaptchaWhitelistIP = array('192.168.1.0/24', '10.1.0.0/16'); * $wgCaptchaBypassIPs = [ '192.168.1.0/24', '10.1.0.0/16' ];
*/ */
$wgCaptchaWhitelistIP = false; $wgCaptchaBypassIPs = false;
/** /**
* Actions which can trigger a captcha * Actions which can trigger a captcha
@ -132,12 +132,12 @@ $wgAllowConfirmedEmail = false;
$wgCaptchaBadLoginAttempts = 3; $wgCaptchaBadLoginAttempts = 3;
/** /**
* Regex to whitelist URLs to known-good sites... * Regex to ignore URLs to known-good sites...
* For instance: * For instance:
* $wgCaptchaWhitelist = '#^https?://([a-z0-9-]+\\.)?(wikimedia|wikipedia)\.org/#i'; * $wgCaptchaIgnoredUrls = '#^https?://([a-z0-9-]+\\.)?(wikimedia|wikipedia)\.org/#i';
* Local admins can define a whitelist under [[MediaWiki:captcha-addurl-whitelist]] * Local admins can define a local allow list under [[MediaWiki:captcha-addurl-whitelist]]
*/ */
$wgCaptchaWhitelist = false; $wgCaptchaIgnoredUrls = false;
/** /**
* Additional regexes to check for. Use full regexes; can match things * Additional regexes to check for. Use full regexes; can match things

View file

@ -119,6 +119,11 @@
}, },
"config": { "config": {
"CaptchaWhitelistIP": { "CaptchaWhitelistIP": {
"description": "DEPRECATED! Use CaptchaBypassIPs",
"value": false
},
"CaptchaBypassIPs": {
"description": "A list of IP addresses that can skip the captcha",
"value": false "value": false
}, },
"Captcha": { "Captcha": {
@ -162,6 +167,11 @@
"value": 20 "value": 20
}, },
"CaptchaWhitelist": { "CaptchaWhitelist": {
"description": "DEPRECATED: Use CaptchaIgnoredUrls",
"value": false
},
"CaptchaIgnoredUrls": {
"description": "Urls that won't trigger a captcha",
"value": false "value": false
}, },
"CaptchaRegexes": { "CaptchaRegexes": {

View file

@ -94,7 +94,7 @@ class Hooks implements
) { ) {
$title = $wikiPage->getTitle(); $title = $wikiPage->getTitle();
if ( $title->getText() === 'Captcha-ip-whitelist' && $title->getNamespace() === NS_MEDIAWIKI ) { if ( $title->getText() === 'Captcha-ip-whitelist' && $title->getNamespace() === NS_MEDIAWIKI ) {
$this->cache->delete( $this->cache->makeKey( 'confirmedit', 'ipwhitelist' ) ); $this->cache->delete( $this->cache->makeKey( 'confirmedit', 'ipbypasslist' ) );
} }
return true; return true;

View file

@ -357,27 +357,30 @@ class SimpleCaptcha {
} }
/** /**
* Check if the current IP is allowed to skip captchas. This checks * Check if the current IP is allowed to skip solving a captcha.
* the whitelist from two sources. * This checks the bypass list from two sources.
* 1) From the server-side config array $wgCaptchaWhitelistIP * 1) From the server-side config array $wgCaptchaWhitelistIP (deprecated) or $wgCaptchaBypassIPs
* 2) From the local [[MediaWiki:Captcha-ip-whitelist]] message * 2) From the local [[MediaWiki:Captcha-ip-whitelist]] message
* *
* @return bool true if whitelisted, false if not * @return bool true if the IP can bypass a captcha, false if not
*/ */
private function isIPWhitelisted() { private function canIPBypassCaptcha() {
global $wgCaptchaWhitelistIP, $wgRequest; global $wgCaptchaWhitelistIP, $wgCaptchaBypassIPs, $wgRequest;
$ip = $wgRequest->getIP(); $ip = $wgRequest->getIP();
if ( $wgCaptchaWhitelistIP ) { // Deprecated; to be removed later
if ( IPUtils::isInRanges( $ip, $wgCaptchaWhitelistIP ) ) { if ( $wgCaptchaWhitelistIP && IPUtils::isInRanges( $ip, $wgCaptchaWhitelistIP ) ) {
return true; return true;
}
} }
$whitelistMsg = wfMessage( 'captcha-ip-whitelist' )->inContentLanguage(); if ( $wgCaptchaBypassIPs && IPUtils::isInRanges( $ip, $wgCaptchaBypassIPs ) ) {
if ( !$whitelistMsg->isDisabled() ) { return true;
$whitelistedIPs = $this->getWikiIPWhitelist( $whitelistMsg ); }
if ( IPUtils::isInRanges( $ip, $whitelistedIPs ) ) {
$msg = wfMessage( 'captcha-ip-whitelist' )->inContentLanguage();
if ( !$msg->isDisabled() ) {
$allowedIPs = $this->getWikiIPBypassList( $msg );
if ( IPUtils::isInRanges( $ip, $allowedIPs ) ) {
return true; return true;
} }
} }
@ -386,40 +389,38 @@ class SimpleCaptcha {
} }
/** /**
* Get the on-wiki IP whitelist stored in [[MediaWiki:Captcha-ip-whitelist]] * Get the on-wiki IP bypass list stored on a MediaWiki page from cache if possible.
* page from cache if possible.
* *
* @param Message $msg whitelist Message on wiki * @param Message $msg Message on wiki with IP lists
* @return array whitelisted IP addresses or IP ranges, empty array if no whitelist * @return array Allowed IP addresses or IP ranges, empty array if none
*/ */
private function getWikiIPWhitelist( Message $msg ) { private function getWikiIPBypassList( Message $msg ) {
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$cacheKey = $cache->makeKey( 'confirmedit', 'ipwhitelist' ); $cacheKey = $cache->makeKey( 'confirmedit', 'ipbypasslist' );
$cachedWhitelist = $cache->get( $cacheKey ); $cached = $cache->get( $cacheKey );
if ( $cachedWhitelist === false ) { if ( $cached !== false ) {
// Could not retrieve from cache so build the whitelist directly return $cached;
// from the wikipage
$whitelist = $this->buildValidIPs(
explode( "\n", $msg->plain() )
);
// And then store it in cache for one day. This cache is cleared on
// modifications to the whitelist page.
// @see MediaWiki\Extension\ConfirmEdit\Hooks::onPageSaveComplete()
$cache->set( $cacheKey, $whitelist, 86400 );
} else {
// Whitelist from the cache
$whitelist = $cachedWhitelist;
} }
return $whitelist; // Could not retrieve from cache, so build the list directly
// from the MediaWiki page
$list = $this->buildValidIPs(
explode( "\n", $msg->plain() )
);
// And then store it in cache for one day.
// This cache is cleared on modifications to the wiki page.
// @see MediaWiki\Extension\ConfirmEdit\Hooks::onPageSaveComplete()
$cache->set( $cacheKey, $list, 86400 );
return $list;
} }
/** /**
* From a list of unvalidated input, get all the valid * From a list of unvalidated input, get all the valid
* IP addresses and IP ranges from it. * IP addresses and IP ranges from it.
* *
* Note that only lines with just the IP address or IP range is considered * Note that only lines with just the IP address or the IP range is considered
* as valid. Whitespace is allowed, but if there is any other character on * as valid. Whitespace is allowed, but if there is any other character on
* the line, it's not considered as a valid entry. * the line, it's not considered as a valid entry.
* *
@ -706,12 +707,12 @@ class SimpleCaptcha {
} }
/** /**
* Filter callback function for URL whitelisting * Filter callback function for URL allow-listing
* @param string $url string to check * @param string $url string to check
* @return bool true if unknown, false if whitelisted * @return bool true if unknown, false if allowed
*/ */
private function filterLink( $url ) { private function filterLink( $url ) {
global $wgCaptchaWhitelist; global $wgCaptchaWhitelist, $wgCaptchaIgnoredUrls;
static $regexes = null; static $regexes = null;
if ( $regexes === null ) { if ( $regexes === null ) {
@ -721,9 +722,13 @@ class SimpleCaptcha {
? [] ? []
: $this->buildRegexes( explode( "\n", $source->plain() ) ); : $this->buildRegexes( explode( "\n", $source->plain() ) );
// DEPRECATED
if ( $wgCaptchaWhitelist !== false ) { if ( $wgCaptchaWhitelist !== false ) {
array_unshift( $regexes, $wgCaptchaWhitelist ); array_unshift( $regexes, $wgCaptchaWhitelist );
} }
if ( $wgCaptchaIgnoredUrls !== false ) {
array_unshift( $regexes, $wgCaptchaIgnoredUrls );
}
} }
foreach ( $regexes as $regex ) { foreach ( $regexes as $regex ) {
@ -736,8 +741,8 @@ class SimpleCaptcha {
} }
/** /**
* Build regex from whitelist * Build regex from list of URLs
* @param string[] $lines string from [[MediaWiki:Captcha-addurl-whitelist]] * @param string[] $lines string from MediaWiki page
* @return string[] Regexes * @return string[] Regexes
* @private * @private
*/ */
@ -749,12 +754,12 @@ class SimpleCaptcha {
$lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) ); $lines = array_filter( array_map( 'trim', preg_replace( '/#.*$/', '', $lines ) ) );
# No lines, don't make a regex which will match everything # No lines, don't make a regex which will match everything
if ( count( $lines ) == 0 ) { if ( count( $lines ) === 0 ) {
wfDebug( "No lines\n" ); wfDebug( "No lines\n" );
return []; return [];
} }
# Make regex # Make regex
# It's faster using the S modifier even though it will usually only be run once # It's faster using the S modifier even though it will usually only be run once
// $regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')'; // $regex = 'http://+[a-z0-9_\-.]*(' . implode( '|', $lines ) . ')';
// return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si'; // return '/' . str_replace( '/', '\/', preg_replace('|\\\*/|', '/', $regex) ) . '/Si';
@ -1241,8 +1246,8 @@ class SimpleCaptcha {
return true; return true;
} }
if ( $this->isIPWhitelisted() ) { if ( $this->canIPBypassCaptcha() ) {
wfDebug( "ConfirmEdit: user IP is whitelisted" ); wfDebug( "ConfirmEdit: user IP can bypass captcha" );
return true; return true;
} }

View file

@ -140,9 +140,9 @@ class CaptchaTest extends MediaWikiIntegrationTestCase {
} }
/** /**
* @dataProvider provideCanSkipCaptchaIPWhitelisted * @dataProvider provideCanSkipCaptchaBypassIPList
*/ */
public function testCanSkipCaptchaIPWhitelisted( $requestIP, $IPWhitelist, $expected ) { public function testCanSkipCaptchaBypassIP( $requestIP, $list, $expected ) {
$testObject = new SimpleCaptcha(); $testObject = new SimpleCaptcha();
$config = new HashConfig( [ 'AllowConfirmedEmail' => false ] ); $config = new HashConfig( [ 'AllowConfirmedEmail' => false ] );
$request = $this->createMock( WebRequest::class ); $request = $this->createMock( WebRequest::class );
@ -151,14 +151,14 @@ class CaptchaTest extends MediaWikiIntegrationTestCase {
$this->setMwGlobals( [ $this->setMwGlobals( [
'wgRequest' => $request, 'wgRequest' => $request,
] ); ] );
$this->overrideConfigValue( 'CaptchaWhitelistIP', $IPWhitelist ); $this->overrideConfigValue( 'CaptchaBypassIPs', $list );
$actual = $testObject->canSkipCaptcha( RequestContext::getMain()->getUser(), $config ); $actual = $testObject->canSkipCaptcha( RequestContext::getMain()->getUser(), $config );
$this->assertEquals( $expected, $actual ); $this->assertEquals( $expected, $actual );
} }
public static function provideCanSkipCaptchaIPWhitelisted() { public static function provideCanSkipCaptchaBypassIPList() {
return ( [ return ( [
[ '127.0.0.1', [ '127.0.0.1', '127.0.0.2' ], true ], [ '127.0.0.1', [ '127.0.0.1', '127.0.0.2' ], true ],
[ '127.0.0.1', [], false ] [ '127.0.0.1', [], false ]