From f005a5cb4a1930585a85f8443646b459fadd17eb Mon Sep 17 00:00:00 2001 From: Reedy Date: Fri, 10 Apr 2020 23:21:08 +0100 Subject: [PATCH] Add hCaptcha Mostly working from Florian's ReCaptchaNoCaptcha Bug: T249854 Change-Id: I5c3ac71bdfd528339b846b5811e78a88e8135e46 --- README.md | 1 + hCaptcha/extension.json | 37 +++ hCaptcha/i18n/en.json | 13 + hCaptcha/i18n/qqq.json | 13 + hCaptcha/includes/HCaptcha.php | 231 ++++++++++++++++++ .../HCaptchaAuthenticationRequest.php | 35 +++ hCaptcha/includes/HTMLHCaptchaField.php | 49 ++++ 7 files changed, 379 insertions(+) create mode 100644 hCaptcha/extension.json create mode 100644 hCaptcha/i18n/en.json create mode 100644 hCaptcha/i18n/qqq.json create mode 100644 hCaptcha/includes/HCaptcha.php create mode 100644 hCaptcha/includes/HCaptchaAuthenticationRequest.php create mode 100644 hCaptcha/includes/HTMLHCaptchaField.php diff --git a/README.md b/README.md index b458503aa..04eee296f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ in a stylized way questions defined by the administrator(s) * ReCaptchaNoCaptcha - users have to solve different types of visually or audially tasks. +* hCaptcha - users have to solve visual tasks ### License diff --git a/hCaptcha/extension.json b/hCaptcha/extension.json new file mode 100644 index 000000000..040afcae5 --- /dev/null +++ b/hCaptcha/extension.json @@ -0,0 +1,37 @@ +{ + "name": "hCaptcha", + "author": [ + "Sam Reed", + "..." + ], + "url": "https://www.mediawiki.org/wiki/Extension:ConfirmEdit", + "descriptionmsg": "hcaptcha-desc", + "license-name": "GPL-2.0-or-later", + "type": "antispam", + "MessagesDirs": { + "hNoCaptcha": [ + "i18n" + ] + }, + "AutoloadNamespaces": { + "MediaWiki\\Extensions\\ConfirmEdit\\hCaptcha\\": "includes/" + }, + "config": { + "CaptchaClass": { + "value": "MediaWiki\\Extensions\\ConfirmEdit\\hCaptcha\\HCaptcha" + }, + "HCaptchaSiteKey": { + "description": "Sitekey from hCaptcha (requires creating an account)", + "value": "" + }, + "HCaptchaSecretKey": { + "description": "Secret key from hCaptcha (requires creating an account)", + "value": "" + }, + "HCaptchaSendRemoteIP": { + "description": "Whether to send the client's IP address to hCaptcha", + "value": false + } + }, + "manifest_version": 2 +} diff --git a/hCaptcha/i18n/en.json b/hCaptcha/i18n/en.json new file mode 100644 index 000000000..e19f80d2a --- /dev/null +++ b/hCaptcha/i18n/en.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [] + }, + "hcaptcha-desc": "[https://www.hcaptcha.com/ hCaptcha] module for Confirm Edit", + "hcaptcha-edit": "To protect the wiki against automated edit spam, we kindly ask you to solve the following hCaptcha:", + "hcaptcha-addurl": "Your edit includes new external links. To protect the wiki against automated spam, we kindly ask you to solve the following CAPTCHA:", + "hcaptcha-badlogin": "To protect the wiki against automated password cracking, we kindly ask you to solve the following hCaptcha:", + "hcaptcha-createaccount": "To protect the wiki against automated account creation, we kindly ask you to solve the following hCaptcha:", + "hcaptcha-createaccount-fail": "It seems you haven't solved the hCaptcha.", + "hcaptcha-create": "To protect the wiki against automated page creation, we kindly ask you to solve the following hCaptcha:", + "hcaptcha-help": "Please solve a hCaptcha challenge and return the response value as captchaWord." +} diff --git a/hCaptcha/i18n/qqq.json b/hCaptcha/i18n/qqq.json new file mode 100644 index 000000000..a93931457 --- /dev/null +++ b/hCaptcha/i18n/qqq.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [] + }, + "hcaptcha-desc": "{{Optional}}\n{{desc}}", + "hcaptcha-edit": "Message above the CAPTCHA for edit action.\n{{related|ConfirmEdit-edit}}", + "hcaptcha-addurl": "Message above the CAPTCHA for addurl (user added new external links to the page) action.\n{{related|ConfirmEdit-addurl}}", + "hcaptcha-badlogin": "Message above the CAPTCHA for badlogin action.\n{{related|ConfirmEdit-badlogin}}", + "hcaptcha-createaccount": "Message above the CAPTCHA for createaccount (user creates a new account) action.\n{{related|ConfirmEdit-createaccount}}", + "hcaptcha-createaccount-fail": "Error message, when the CAPTCHA isn't solved correctly.\n{{related|ConfirmEdit-createaccount-fail}}", + "hcaptcha-create": "Message above the CAPTCHA for create (user creates a new page) action.\n{{related|ConfirmEdit-create}}", + "hcaptcha-help": "Explanation of how to solve the CAPTCHA for API clients." +} diff --git a/hCaptcha/includes/HCaptcha.php b/hCaptcha/includes/HCaptcha.php new file mode 100644 index 000000000..e87a54870 --- /dev/null +++ b/hCaptcha/includes/HCaptcha.php @@ -0,0 +1,231 @@ + [ + 'h-captcha', + 'mw-confirmedit-captcha-fail' => (bool)$this->error, + ], + 'data-sitekey' => $wgHCaptchaSiteKey + ] ); + + return [ + 'html' => $output, + 'headitems' => [ + "" + ] + ]; + } + + /** + * @param Status|array|string $info + */ + protected function logCheckError( $info ) { + if ( $info instanceof Status ) { + $errors = $info->getErrorsArray(); + $error = $errors[0][0]; + } elseif ( is_array( $info ) ) { + $error = implode( ',', $info ); + } else { + $error = $info; + } + + \wfDebugLog( 'captcha', 'Unable to validate response: ' . $error ); + } + + /** + * @param WebRequest $request + * @return array + */ + protected function getCaptchaParamsFromRequest( WebRequest $request ) { + $response = $request->getVal( 'h-captcha-response' ); + return [ '', $response ]; + } + + /** + * Check, if the user solved the captcha. + * + * Based on reference implementation: + * https://github.com/google/recaptcha#php and https://docs.hcaptcha.com/ + * + * @param mixed $_ Not used + * @param string $token token from the POST data + * @return bool + */ + protected function passCaptcha( $_, $token ) { + global $wgRequest, $wgHCaptchaSecretKey, $wgHCaptchaSendRemoteIP; + + $url = 'https://hcaptcha.com/siteverify'; + $data = [ + 'secret' => $wgHCaptchaSecretKey, + 'response' => $token, + ]; + if ( $wgHCaptchaSendRemoteIP ) { + $data['remoteip'] = $wgRequest->getIP(); + } + $request = MWHttpRequest::factory( + $url, + [ + 'method' => 'POST', + 'postData' => $data, + ] + ); + $status = $request->execute(); + if ( !$status->isOK() ) { + $this->error = 'http'; + $this->logCheckError( $status ); + return false; + } + $response = FormatJson::decode( $request->getContent(), true ); + if ( !$response ) { + $this->error = 'json'; + $this->logCheckError( $this->error ); + return false; + } + if ( isset( $response['error-codes'] ) ) { + $this->error = 'hcaptcha-api'; + $this->logCheckError( $response['error-codes'] ); + return false; + } + + return $response['success']; + } + + /** + * @param array &$resultArr + */ + protected function addCaptchaAPI( &$resultArr ) { + } + + /** + * @return array + */ + public function describeCaptchaType() { + global $wgHCaptchaSiteKey; + return [ + 'type' => 'hcaptcha', + 'mime' => 'application/javascript', + 'key' => $wgHCaptchaSiteKey, + ]; + } + + /** + * Show a message asking the user to enter a captcha on edit + * The result will be treated as wiki text + * + * @param string $action Action being performed + * @return Message + */ + public function getMessage( $action ) { + $msg = parent::getMessage( $action ); + if ( $this->error ) { + $msg = new RawMessage( '
$1
', [ $msg ] ); + } + return $msg; + } + + /** + * @param ApiBase $module + * @param array &$params + * @param int $flags + * @return bool + */ + public function apiGetAllowedParams( ApiBase $module, &$params, $flags ) { + return true; + } + + /** + * @inheritDoc + */ + public function getError() { + return $this->error; + } + + /** + * @inheritDoc + */ + public function storeCaptcha( $info ) { + return 'not used'; + } + + /** + * @inheritDoc + */ + public function retrieveCaptcha( $index ) { + // just pretend it worked + return [ 'index' => $index ]; + } + + /** + * @inheritDoc + */ + public function getCaptcha() { + return []; + } + + /** + * @return HCaptchaAuthenticationRequest + */ + public function createAuthenticationRequest() { + return new HCaptchaAuthenticationRequest(); + } + + /** + * @param array $requests + * @param array $fieldInfo + * @param array &$formDescriptor + * @param string $action + */ + public function onAuthChangeFormFields( + array $requests, array $fieldInfo, array &$formDescriptor, $action + ) { + global $wgHCaptchaSiteKey; + + $req = AuthenticationRequest::getRequestByClass( + $requests, + CaptchaAuthenticationRequest::class, + true + ); + if ( !$req ) { + return; + } + + // ugly way to retrieve error information + $captcha = ConfirmEditHooks::getInstance(); + + $formDescriptor['captchaWord'] = [ + 'class' => HTMLHCaptchaField::class, + 'key' => $wgHCaptchaSiteKey, + 'error' => $captcha->getError(), + ] + $formDescriptor['captchaWord']; + } +} diff --git a/hCaptcha/includes/HCaptchaAuthenticationRequest.php b/hCaptcha/includes/HCaptchaAuthenticationRequest.php new file mode 100644 index 000000000..c47ec8056 --- /dev/null +++ b/hCaptcha/includes/HCaptchaAuthenticationRequest.php @@ -0,0 +1,35 @@ + [ + 'type' => 'string', + 'label' => $fieldInfo['captchaInfo']['label'], + 'help' => \wfMessage( 'hcaptcha-help' ), + ], + ]; + } +} diff --git a/hCaptcha/includes/HTMLHCaptchaField.php b/hCaptcha/includes/HTMLHCaptchaField.php new file mode 100644 index 000000000..f9b5ea263 --- /dev/null +++ b/hCaptcha/includes/HTMLHCaptchaField.php @@ -0,0 +1,49 @@ + null ]; + parent::__construct( $params ); + + $this->key = $params['key']; + $this->error = $params['error']; + + $this->mName = 'h-captcha-response'; + } + + /** + * @inheritDoc + */ + public function getInputHTML( $value ) { + $out = $this->mParent->getOutput(); + + $out->addHeadItem( + 'h-captcha', + "" + ); + return Html::element( 'div', [ + 'class' => [ + 'h-captcha', + 'mw-confirmedit-captcha-fail' => (bool)$this->error, + ], + 'data-sitekey' => $this->key, + ] ); + } +}