diff --git a/README.md b/README.md
index a71d3f2..69c8632 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
## Introduction
This plugin logs failed login attempts and requires users to go through
-a reCAPTCHA verification process when the number of failed attempts go
+a reCAPTCHA|hCaptcha verification process when the number of failed attempts go
too high. It provides protection against automated attacks.
Failed attempts are logged by IP and stored in a database table.
@@ -12,8 +12,8 @@ IPs are also released after a certain expire amount of time.
## Installation
-**IMPORTANT: This plugin requires reCAPTCHA API keys to work properly.**
-
These can be obtained from https://www.google.com/recaptcha.
+**IMPORTANT: This plugin requires reCAPTCHA|hCaptcha API keys to work properly.**
+
These can be obtained from https://www.google.com/recaptcha or https://dashboard.hcaptcha.com/.
#### With Composer
@@ -44,15 +44,16 @@ the table `rcguard` accordingly.
You may customize the following in the `config.inc.php` file:
-- the API version: `v3`, `v2invisible` or `v2`;
+- the API version: `v3`, `v2invisible`, `v2` or `v2hcaptcha`;
- the v2 widget theme: `light` or `dark`;
- the v2 widget size: `normal` or `compact`.
-For more information about the widget please check the [documentation about reCAPTCHA][recaptcha-doc].
+For more information about the widget please check the [documentation about reCAPTCHA][recaptcha-doc]
+or [documentation about hCaptcha][hcaptcha-doc].
The plugin configuration file has several other options you may configure, please take at look.
-Since May 2018, you can define a proxy (anonymous or authenticated) to request the recaptcha widget.
+Since May 2018, you can define a proxy (anonymous or authenticated) to request the reCAPTCHA|hCaptcha widget.
## Supported databases
@@ -79,12 +80,13 @@ Email: [Diana Soares][email]
[email]: mailto:diana.soares@gmail.com
[dennylin]: https://github.com/dennylin93
[recaptcha-doc]: https://developers.google.com/recaptcha/intro
+[hcaptcha-doc]: https://docs.hcaptcha.com/
## License
This plugin is distributed under the GPL-3.0+ license.
-This plugin also contains a PHP library for reCAPTCHA that is
-distributed under its own license. See the library file for the exact details.
+This plugin also contains PHP libraries for reCAPTCHA and hCaptcha that is
+distributed under its own license. See the library files for the exact details.
diff --git a/config.inc.php.dist b/config.inc.php.dist
index 94616d4..d6843fb 100644
--- a/config.inc.php.dist
+++ b/config.inc.php.dist
@@ -12,21 +12,22 @@ $config['expire_time'] = 30;
// Reset failure count after successfull login (see bratkartoffel/rcguard@670395e)
$config['rcguard_reset_after_success'] = true;
-// reCAPTCHA API version and url
-$config['recaptcha_api_version'] = 'v2'; // v3 | v2 | v2invisible
-$config['recaptcha_api_url'] = 'https://www.google.com/recaptcha/api.js';
+// reCAPTCHA|hCaptcha API version and url
+$config['recaptcha_api_version'] = 'v2'; // v3 | v2 | v2invisible | v2hcaptcha
+$config['recaptcha_api_url'] = 'https://www.google.com/recaptcha/api.js'; // reCAPTCHA
+//$config['recaptcha_api_url'] = 'https://js.hcaptcha.com/1/api.js'; // hCaptcha
// !!! DEPRECATED - not used anymore !!!
//$config['recaptcha_api'] = 'http://www.google.com/recaptcha/api.js';
//$config['recaptcha_api_secure'] = 'https://www.google.com/recaptcha/api.js';
//$config['recaptcha_https'] = true;
-// Keys can be obtained from http://www.google.com/recaptcha/
+// Keys can be obtained from http://www.google.com/recaptcha/ or https://dashboard.hcaptcha.com/
-// reCAPTCHA site key
+// reCAPTCHA|hCaptcha site key
$config['recaptcha_publickey'] = '';
-// reCAPTCHA secret key
+// reCAPTCHA|hCaptcha secret key
$config['recaptcha_privatekey'] = '';
// Send client IP to Google for reCAPTCHA verification
@@ -53,8 +54,9 @@ $config['recaptcha_size'] = 'normal';
// Parameter expansion:
// %r - Remote IP
// %u - Username
-$config['recaptcha_log_success'] = 'Verification succeeded for %u. [%r]';
-$config['recaptcha_log_failure'] = 'Error: Verification failed for %u. [%r]';
+// %v - API version
+$config['recaptcha_log_success'] = 'Verification succeeded for %u. [%r] via %v';
+$config['recaptcha_log_failure'] = 'Error: Verification failed for %u. [%r] via %v';
$config['recaptcha_log_unknown'] = 'Error: Unknown log type.';
// Block IPv6 clients based on prefix length
diff --git a/lib/hcaptchalib.php b/lib/hcaptchalib.php
new file mode 100644
index 0000000..0e4f7cf
--- /dev/null
+++ b/lib/hcaptchalib.php
@@ -0,0 +1,196 @@
+' . self::$signupUrl . '');
+ }
+
+ $this->_secret = $secret;
+ $this->_options = $extra_options;
+ }
+
+
+ /**
+ * Submit the POST request with the specified parameters.
+ *
+ * @param array $params Request parameters
+ * @return string Body of the hCaptcha response
+ */
+ private function _submit($params)
+ {
+ // PHP 5.6.0 changed the way you specify the peer name for SSL context options.
+ // Using "CN_name" will still work, but it will raise deprecated errors.
+ $peer_key = version_compare(PHP_VERSION, '5.6.0', '<') ? 'CN_name' : 'peer_name';
+ $options = array(
+ 'http' => array(
+ 'header' => "Content-type: application/x-www-form-urlencoded\r\n",
+ 'method' => 'POST',
+ 'content' => http_build_query($params, '', '&'),
+ // Force the peer to validate (not needed in 5.6.0+, but still works)
+ 'verify_peer' => true,
+ $peer_key => 'hcaptcha.com',
+ )
+ );
+
+ if ($this->_options) {
+ $options = self::mergeOptions($options, $this->_options);
+ }
+
+ // REMEMBER: this is only for this kind of RequestMethod\Post
+ if (isset($options['http']['proxy'])
+ && strpos($options['http']['proxy'], 'tcp://') === false) {
+ $options['http']['proxy'] = 'tcp://' . $options['http']['proxy'];
+ }
+
+ $context = stream_context_create($options);
+ return file_get_contents(self::$siteVerifyUrl, false, $context);
+ }
+
+ /**
+ * Recursively merge options without appending values.
+ *
+ * @param array $opts1 Options array (the default options)
+ * @param array $opts2 Options array (the given options)
+ * @return array The merged options
+ */
+ private static function mergeOptions($opts1, $opts2)
+ {
+ if (is_array($opts2)) {
+ foreach ($opts2 as $key => $val) {
+ $opts1[$key] = (
+ is_array($val) && isset($opts1[$key]) && is_array($opts1[$key])
+ ? self::mergeOptions($opts1[$key], $val) : $val
+ );
+ }
+ }
+ return $opts1;
+ }
+
+ /**
+ * Calls the hCaptcha siteverify API to verify whether the user passes
+ * CAPTCHA test. (hCaptcha version php_1.1.1)
+ *
+ * @param string $response The value of 'h-captcha-response' in the submitted form.
+ * @param string $remoteIp The end user's IP address.
+ * @param string $sitekey assigned site key
+ * @return ReCaptchaResponse Response from the service.
+ */
+ public function verify($response, $remoteIp = null, $sitekey = null)
+ {
+ if (empty($response)) { // Discard empty solution submissions
+ return new ReCaptchaResponse(false, array('missing-input'));
+ }
+
+ $params = array('secret' => $this->_secret,
+ 'sitekey' => $sitekey,
+ 'remoteip' => $remoteIp,
+ 'response' => $response
+ );
+
+ $rawResponse = $this->_submit($params);
+
+ return ReCaptchaResponse::fromJson($rawResponse);
+ }
+}
+
+
+/**
+ * The response returned from the service.
+ */
+class ReCaptchaResponse
+{
+ public $success;
+ public $errorCodes;
+
+ /**
+ * Constructor.
+ *
+ * @param boolean $success
+ * @param array $errorCodes
+ */
+ function __construct($success, $errorCodes=array())
+ {
+ $this->success = $success;
+ $this->errorCodes = $errorCodes;
+ }
+
+ /**
+ * Build the response from the expected JSON returned by the service.
+ *
+ * @param string $json
+ * @return ReCaptchaResponse
+ */
+ public static function fromJson($json)
+ {
+ $responseData = json_decode($json, true);
+
+ if (!$responseData) {
+ $reCaptchaResponse = new ReCaptchaResponse(false, array('invalid-json'));
+ }
+ else if (isset($responseData['success']) && $responseData['success'] == true) {
+ $reCaptchaResponse = new ReCaptchaResponse(true);
+ }
+ else if (isset($responseData['error-codes']) && is_array($responseData['error-codes'])) {
+ $reCaptchaResponse = new ReCaptchaResponse(false, $responseData['error-codes']);
+ }
+ else {
+ $reCaptchaResponse = new ReCaptchaResponse(false);
+ }
+
+ return $reCaptchaResponse;
+ }
+}
+
+?>
diff --git a/rcguard.php b/rcguard.php
index f6aaf6a..d9507e0 100644
--- a/rcguard.php
+++ b/rcguard.php
@@ -123,7 +123,14 @@ public function authenticate($args)
}
$msg = 'rcguard.recaptchaempty';
- $response = rcube_utils::get_input_value('g-recaptcha-response', rcube_utils::INPUT_POST);
+
+ $api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
+ $input_value = 'g-recaptcha-response';
+ if ($api_version == 'v2hcaptcha') {
+ $input_value = 'h-captcha-response';
+ };
+
+ $response = rcube_utils::get_input_value($input_value, rcube_utils::INPUT_POST);
if ($response) {
if ($this->verify_recaptcha($response, $client_ip)) {
@@ -310,12 +317,20 @@ private function show_recaptcha_v2invisible()
private function show_recaptcha_v2($size = null)
{
$api = $this->rc->config->get('recaptcha_api_url');
- $src = sprintf('%s?hl=%s', $api, $this->rc->user->language);
+ $lang = $this->rc->user->language;
+ $lang_territory_separator_pos = strpos($lang, '_');
+ if ($lang_territory_separator_pos > 0) {
+ // hCaptcha is not supporting 'territory' appendix
+ $lang = substr($lang, 0, $lang_territory_separator_pos);
+ };
+ $src = sprintf('%s?hl=%s', $api, $lang);
$this->include_script($src);
+ $api_version = $this->rc->config->get('recaptcha_api_version', 'v2');
$html = sprintf(
- '