mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/ConfirmEdit
synced 2024-11-27 17:40:11 +00:00
Support hCaptcha for VisualEditor
Extend Captcha save error handler to verify user using
hCaptcha.
Bug: T264684
Change-Id: I88928e291a2ecd6edd74af62575e8704c0a2ee13
(cherry picked from commit 0993a43a29
)
This commit is contained in:
parent
c6c0316abb
commit
7850e41f03
|
@ -9,7 +9,7 @@
|
|||
"license-name": "GPL-2.0-or-later",
|
||||
"type": "antispam",
|
||||
"MessagesDirs": {
|
||||
"hNoCaptcha": [
|
||||
"HCaptcha": [
|
||||
"i18n"
|
||||
]
|
||||
},
|
||||
|
@ -37,5 +37,36 @@
|
|||
"value": false
|
||||
}
|
||||
},
|
||||
"ConfigRegistry": {
|
||||
"hcaptcha": "GlobalVarConfig::newInstance"
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
"localBasePath": "resources",
|
||||
"remoteExtPath": "ConfirmEdit/hCaptcha/resources"
|
||||
},
|
||||
"ResourceModules": {
|
||||
"ext.confirmEdit.hCaptcha.visualEditor": {
|
||||
"scripts": "ve-confirmedit-hCaptcha/ve.init.mw.HCaptchaSaveErrorHandler.js",
|
||||
"targets": [
|
||||
"desktop",
|
||||
"mobile"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Hooks": {
|
||||
"ResourceLoaderGetConfigVars": "resourceloader"
|
||||
},
|
||||
"HookHandlers": {
|
||||
"resourceloader": {
|
||||
"class": "MediaWiki\\Extensions\\ConfirmEdit\\hCaptcha\\Hooks\\ResourceLoaderHooks"
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"VisualEditor": {
|
||||
"PluginModules": [
|
||||
"ext.confirmEdit.hCaptcha.visualEditor"
|
||||
]
|
||||
}
|
||||
},
|
||||
"manifest_version": 2
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ namespace MediaWiki\Extensions\ConfirmEdit\hCaptcha;
|
|||
use ApiBase;
|
||||
use CaptchaAuthenticationRequest;
|
||||
use ConfirmEditHooks;
|
||||
use ContentSecurityPolicy;
|
||||
use FormatJson;
|
||||
use Html;
|
||||
use MediaWiki\Auth\AuthenticationRequest;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Message;
|
||||
use RawMessage;
|
||||
use RequestContext;
|
||||
use SimpleCaptcha;
|
||||
use Status;
|
||||
use WebRequest;
|
||||
|
@ -20,7 +22,17 @@ class HCaptcha extends SimpleCaptcha {
|
|||
// hcaptcha-create, hcaptcha-sendemail via getMessage()
|
||||
protected static $messagePrefix = 'hcaptcha-';
|
||||
|
||||
private $error;
|
||||
private $error = null;
|
||||
|
||||
private $hCaptchaConfig;
|
||||
|
||||
private $siteKey;
|
||||
|
||||
public function __construct() {
|
||||
$this->hCaptchaConfig = MediaWikiServices::getInstance()->getConfigFactory()
|
||||
->makeConfig( 'hcaptcha' );
|
||||
$this->siteKey = $this->hCaptchaConfig->get( 'HCaptchaSiteKey' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the captcha form.
|
||||
|
@ -28,14 +40,12 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @return array
|
||||
*/
|
||||
public function getFormInformation( $tabIndex = 1 ) {
|
||||
global $wgHCaptchaSiteKey;
|
||||
|
||||
$output = Html::element( 'div', [
|
||||
'class' => [
|
||||
'h-captcha',
|
||||
'mw-confirmedit-captcha-fail' => (bool)$this->error,
|
||||
],
|
||||
'data-sitekey' => $wgHCaptchaSiteKey
|
||||
'data-sitekey' => $this->siteKey
|
||||
] );
|
||||
|
||||
return [
|
||||
|
@ -53,6 +63,22 @@ class HCaptcha extends SimpleCaptcha {
|
|||
return [ 'https://hcaptcha.com', 'https://*.hcaptcha.com' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the CSP policies necessary for the captcha module to work in a CSP enforced
|
||||
* setup.
|
||||
*
|
||||
* @param ContentSecurityPolicy $csp The CSP instance to add the policies to, usually
|
||||
* obtained from {@link OutputPage::getCSP()}
|
||||
*/
|
||||
public static function addCSPSources( ContentSecurityPolicy $csp ) {
|
||||
foreach ( static::getCSPUrls() as $src ) {
|
||||
// Since frame-src is not supported
|
||||
$csp->addDefaultSrc( $src );
|
||||
$csp->addScriptSrc( $src );
|
||||
$csp->addStyleSrc( $src );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Status|array|string $info
|
||||
*/
|
||||
|
@ -74,7 +100,10 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @return array
|
||||
*/
|
||||
protected function getCaptchaParamsFromRequest( WebRequest $request ) {
|
||||
$response = $request->getVal( 'h-captcha-response' );
|
||||
$response = $request->getVal(
|
||||
'h-captcha-response',
|
||||
$request->getVal( 'captchaWord', $request->getVal( 'captchaword' ) )
|
||||
);
|
||||
return [ '', $response ];
|
||||
}
|
||||
|
||||
|
@ -89,15 +118,19 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @return bool
|
||||
*/
|
||||
protected function passCaptcha( $_, $token ) {
|
||||
global $wgRequest, $wgHCaptchaSecretKey, $wgHCaptchaSendRemoteIP, $wgHCaptchaProxy;
|
||||
$webRequest = RequestContext::getMain()->getRequest();
|
||||
|
||||
$secretKey = $this->hCaptchaConfig->get( 'HCaptchaSecretKey' );
|
||||
$sendRemoteIp = $this->hCaptchaConfig->get( 'HCaptchaSendRemoteIP' );
|
||||
$proxy = $this->hCaptchaConfig->get( 'HCaptchaProxy' );
|
||||
|
||||
$url = 'https://hcaptcha.com/siteverify';
|
||||
$data = [
|
||||
'secret' => $wgHCaptchaSecretKey,
|
||||
'secret' => $secretKey,
|
||||
'response' => $token,
|
||||
];
|
||||
if ( $wgHCaptchaSendRemoteIP ) {
|
||||
$data['remoteip'] = $wgRequest->getIP();
|
||||
if ( $sendRemoteIp ) {
|
||||
$data['remoteip'] = $webRequest->getIP();
|
||||
}
|
||||
|
||||
$options = [
|
||||
|
@ -105,8 +138,8 @@ class HCaptcha extends SimpleCaptcha {
|
|||
'postData' => $data,
|
||||
];
|
||||
|
||||
if ( $wgHCaptchaProxy ) {
|
||||
$options['proxy'] = $wgHCaptchaProxy;
|
||||
if ( $proxy ) {
|
||||
$options['proxy'] = $proxy;
|
||||
}
|
||||
|
||||
$request = MediaWikiServices::getInstance()->getHttpRequestFactory()
|
||||
|
@ -137,17 +170,18 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @param array &$resultArr
|
||||
*/
|
||||
protected function addCaptchaAPI( &$resultArr ) {
|
||||
$resultArr['captcha'] = $this->describeCaptchaType();
|
||||
$resultArr['captcha']['error'] = $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function describeCaptchaType() {
|
||||
global $wgHCaptchaSiteKey;
|
||||
return [
|
||||
'type' => 'hcaptcha',
|
||||
'mime' => 'application/javascript',
|
||||
'key' => $wgHCaptchaSiteKey,
|
||||
'key' => $this->siteKey,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -187,6 +221,8 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @inheritDoc
|
||||
*/
|
||||
public function storeCaptcha( $info ) {
|
||||
// hCaptcha is stored externally, the ID will be generated at that time as well, and
|
||||
// the one returned here won't be used. Just pretend this worked.
|
||||
return 'not used';
|
||||
}
|
||||
|
||||
|
@ -202,6 +238,7 @@ class HCaptcha extends SimpleCaptcha {
|
|||
* @inheritDoc
|
||||
*/
|
||||
public function getCaptcha() {
|
||||
// hCaptcha is handled by frontend code + an external provider; nothing to do here.
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -221,8 +258,6 @@ class HCaptcha extends SimpleCaptcha {
|
|||
public function onAuthChangeFormFields(
|
||||
array $requests, array $fieldInfo, array &$formDescriptor, $action
|
||||
) {
|
||||
global $wgHCaptchaSiteKey;
|
||||
|
||||
$req = AuthenticationRequest::getRequestByClass(
|
||||
$requests,
|
||||
CaptchaAuthenticationRequest::class,
|
||||
|
@ -237,7 +272,7 @@ class HCaptcha extends SimpleCaptcha {
|
|||
|
||||
$formDescriptor['captchaWord'] = [
|
||||
'class' => HTMLHCaptchaField::class,
|
||||
'key' => $wgHCaptchaSiteKey,
|
||||
'key' => $this->siteKey,
|
||||
'error' => $captcha->getError(),
|
||||
] + $formDescriptor['captchaWord'];
|
||||
}
|
||||
|
|
29
hCaptcha/includes/Hooks/ResourceLoaderHooks.php
Normal file
29
hCaptcha/includes/Hooks/ResourceLoaderHooks.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace MediaWiki\Extensions\ConfirmEdit\hCaptcha\Hooks;
|
||||
|
||||
use Config;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
|
||||
|
||||
class ResourceLoaderHooks implements ResourceLoaderGetConfigVarsHook {
|
||||
/**
|
||||
* Adds extra variables to the global config
|
||||
*
|
||||
* @param array &$vars Global variables object
|
||||
* @param string $skin
|
||||
* @param Config $config
|
||||
* @return void
|
||||
*/
|
||||
public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ) : void {
|
||||
$hCaptchaConfig = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'hcaptcha' );
|
||||
if ( $hCaptchaConfig->get( 'CaptchaClass' ) === 'MediaWiki\\Extensions\\ConfirmEdit\\hCaptcha\\HCaptcha' ) {
|
||||
$vars['wgConfirmEditConfig'] = [
|
||||
'hCaptchaSiteKey' => $hCaptchaConfig->get( 'HCaptchaSiteKey' ),
|
||||
'hCaptchaScriptURL' => 'https://hcaptcha.com/1/api.js',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"globals": {
|
||||
"ve": false,
|
||||
"OO": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
mw.loader.using( 'ext.visualEditor.targetLoader' ).then( function () {
|
||||
mw.libs.ve.targetLoader.addPlugin( function () {
|
||||
ve.init.mw.HCaptchaSaveErrorHandler = function () {};
|
||||
|
||||
OO.inheritClass( ve.init.mw.HCaptchaSaveErrorHandler, ve.init.mw.SaveErrorHandler );
|
||||
|
||||
ve.init.mw.HCaptchaSaveErrorHandler.static.name = 'confirmEditHCaptcha';
|
||||
|
||||
ve.init.mw.HCaptchaSaveErrorHandler.static.getReadyPromise = function () {
|
||||
var onLoadFn = 'onHcaptchaLoadCallback' + Date.now(),
|
||||
deferred, config, scriptURL, params;
|
||||
|
||||
if ( !this.readyPromise ) {
|
||||
deferred = $.Deferred();
|
||||
config = mw.config.get( 'wgConfirmEditConfig' );
|
||||
scriptURL = new mw.Uri( config.hCaptchaScriptURL );
|
||||
params = { onload: onLoadFn, render: 'explicit' };
|
||||
scriptURL.query = $.extend( scriptURL.query, params );
|
||||
|
||||
this.readyPromise = deferred.promise();
|
||||
window[ onLoadFn ] = deferred.resolve;
|
||||
mw.loader.load( scriptURL.toString() );
|
||||
}
|
||||
|
||||
return this.readyPromise;
|
||||
};
|
||||
|
||||
ve.init.mw.HCaptchaSaveErrorHandler.static.matchFunction = function ( data ) {
|
||||
var captchaData = ve.getProp( data, 'visualeditoredit', 'edit', 'captcha' );
|
||||
|
||||
return !!( captchaData && captchaData.type === 'hcaptcha' );
|
||||
};
|
||||
|
||||
ve.init.mw.HCaptchaSaveErrorHandler.static.process = function ( data, target ) {
|
||||
var self = this,
|
||||
config = mw.config.get( 'wgConfirmEditConfig' ),
|
||||
siteKey = config.hCaptchaSiteKey,
|
||||
$container = $( '<div>' );
|
||||
|
||||
// Register extra fields
|
||||
target.saveFields.wpCaptchaWord = function () {
|
||||
// eslint-disable-next-line no-jquery/no-global-selector
|
||||
return $( '[name=h-captcha-response]' ).val();
|
||||
};
|
||||
|
||||
this.getReadyPromise()
|
||||
.then( function () {
|
||||
// ProcessDialog's error system isn't great for this yet.
|
||||
target.saveDialog.clearMessage( 'api-save-error' );
|
||||
target.saveDialog.showMessage( 'api-save-error', $container, { wrap: false } );
|
||||
self.widgetId = window.hcaptcha.render( $container[ 0 ], {
|
||||
sitekey: siteKey,
|
||||
callback: function () {
|
||||
target.saveDialog.executeAction( 'save' );
|
||||
},
|
||||
'expired-callback': function () {},
|
||||
'error-callback': function () {}
|
||||
} );
|
||||
target.saveDialog.popPending();
|
||||
target.saveDialog.updateSize();
|
||||
|
||||
target.emit( 'saveErrorCaptcha' );
|
||||
} );
|
||||
};
|
||||
|
||||
ve.init.mw.saveErrorHandlerFactory.register( ve.init.mw.HCaptchaSaveErrorHandler );
|
||||
} );
|
||||
} );
|
Loading…
Reference in a new issue