From f68b2ae4ee7405a1aa72f7dcac2104cb453a8ef3 Mon Sep 17 00:00:00 2001 From: David Coutadeur Date: Mon, 24 Jun 2024 20:00:32 +0200 Subject: [PATCH] add the captcha module friendlycaptcha (#895) --- conf/config.inc.php | 7 +- docs/config_general.rst | 14 +++- lib/captcha/FriendlyCaptcha.php | 114 +++++++++++++++++++++++++++++ tests/FriendlyCaptchaTest.php | 123 ++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 lib/captcha/FriendlyCaptcha.php create mode 100644 tests/FriendlyCaptchaTest.php diff --git a/conf/config.inc.php b/conf/config.inc.php index 9613c310..279103cd 100644 --- a/conf/config.inc.php +++ b/conf/config.inc.php @@ -368,7 +368,12 @@ ## Captcha $use_captcha = false; -$captcha_class = "InternalCaptcha"; +#$captcha_class = "InternalCaptcha"; + +#$captcha_class = "FriendlyCaptcha"; +#$friendlycaptcha_apiurl = "https://api.friendlycaptcha.com/api/v1/siteverify"; +#$friendlycaptcha_sitekey = "secret"; +#$friendlycaptcha_secret = "secret"; ## Default action # change diff --git a/docs/config_general.rst b/docs/config_general.rst index 664bc92a..dd8b9981 100644 --- a/docs/config_general.rst +++ b/docs/config_general.rst @@ -288,7 +288,19 @@ You should also define the captcha module to use. For ``$captcha_class``, you can select another captcha module. For now, only ``InternalCaptcha`` and ``FriendlyCaptcha`` are supported. -You can also add your own Captcha module. (see :doc:`developpers` ) +If you want to set up ``FriendlyCaptcha``, you must also configure additional parameters: + +.. code-block:: php + + $use_captcha = true; + $captcha_class = "FriendlyCaptcha"; + $friendlycaptcha_apiurl = "https://api.friendlycaptcha.com/api/v1/siteverify"; + $friendlycaptcha_sitekey = "FC123456789"; + $friendlycaptcha_secret = "secret"; + +See `FriendlyCaptcha documentation `_ for more information + +You can also integrate any other Captcha module by developping the corresponding plugin. (see :doc:`developpers` ) .. |image0| image:: images/br.png diff --git a/lib/captcha/FriendlyCaptcha.php b/lib/captcha/FriendlyCaptcha.php new file mode 100644 index 00000000..934ad6bc --- /dev/null +++ b/lib/captcha/FriendlyCaptcha.php @@ -0,0 +1,114 @@ +friendlycaptcha_apiurl = $friendlycaptcha_apiurl; + $this->friendlycaptcha_sitekey = $friendlycaptcha_sitekey; + $this->friendlycaptcha_secret = $friendlycaptcha_secret; + + # Other stuff to initialize + } + + # Function that insert extra css + function generate_css_captcha(){ + $captcha_css = ' + '; + + return $captcha_css; + } + + # Function that insert extra js + function generate_js_captcha(){ + $captcha_js = ' + + + '; + + return $captcha_js; + } + + # Function that generate the html part containing the captcha + function generate_html_captcha($messages){ + + $captcha_html =' +
+
+
+
+
+
+
'; + + return $captcha_html; + } + + # Function that generate the captcha challenge + # Could be called by the backend, or by a call through a REST API to define + function generate_captcha_challenge(){ + } + + # Function that verify that the result sent by the user + # matches the captcha challenge + function verify_captcha_challenge(){ + $result=""; + if (isset($_POST["frc-captcha-solution"]) and $_POST["frc-captcha-solution"]) { + $captchaphrase = strval($_POST["frc-captcha-solution"]); + + # Call to friendlycaptcha rest api + $data = [ + 'solution' => "$captchaphrase", + 'secret' => $this->friendlycaptcha_secret, + 'sitekey' => $this->friendlycaptcha_sitekey, + ]; + $options = [ + 'http' => [ + 'header' => "Content-type: application/x-www-form-urlencoded\r\n", + 'method' => 'POST', + 'content' => http_build_query($data), + ], + ]; + $context = stream_context_create($options); + $response = file_get_contents($this->friendlycaptcha_apiurl, false, $context); + if ($response === false) { + error_log("Error while reaching ".$this->friendlycaptcha_apiurl); + $result = "badcaptcha"; + } + $json_response = json_decode($response); + if( $json_response->success != "true" ) + { + error_log("Error while verifying captcha $captchaphrase on ".$this->friendlycaptcha_apiurl.": ".var_export($json_response->errors, true)); + $result = "badcaptcha"; + } + else + { + // captcha verified successfully + error_log("Captcha verified successfully: $captchaphrase on ".$this->friendlycaptcha_apiurl.": ".var_export($json_response, true)); + } + + } + else { + $result = "captcharequired"; + } + return $result; + } + +} + + +?> diff --git a/tests/FriendlyCaptchaTest.php b/tests/FriendlyCaptchaTest.php new file mode 100644 index 00000000..d2c99045 --- /dev/null +++ b/tests/FriendlyCaptchaTest.php @@ -0,0 +1,123 @@ +assertEquals('captcha\FriendlyCaptcha', get_class($captchaInstance), "Wrong class"); + } + + public function test_generate_js_captcha(): void + { + $friendlycaptcha_apiurl = 'http://127.0.0.1/'; + $friendlycaptcha_sitekey = 'FC12345'; + $friendlycaptcha_secret = 'secret'; + + $captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl, + $friendlycaptcha_sitekey, + $friendlycaptcha_secret); + + $js = $captchaInstance->generate_js_captcha(); + + $this->assertMatchesRegularExpression('/https:\/\/cdn.jsdelivr.net\/npm\/friendly-challenge/i',$js, "dummy js code returned"); + } + + public function test_generate_html_captcha(): void + { + $messages = array(); + + $friendlycaptcha_apiurl = 'http://127.0.0.1/'; + $friendlycaptcha_sitekey = 'FC12345'; + $friendlycaptcha_secret = 'secret'; + + $captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl, + $friendlycaptcha_sitekey, + $friendlycaptcha_secret); + + $html = $captchaInstance->generate_html_captcha($messages); + + $this->assertMatchesRegularExpression('/
/',$html, "dummy challenge in html code"); + } + + public function test_verify_captcha_challenge_ok(): void + { + + $friendlycaptcha_apiurl = 'http://127.0.0.1/'; + $friendlycaptcha_sitekey = 'FC12345'; + $friendlycaptcha_secret = 'secret'; + $http_response = '{"success": "true"}'; + + $captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl, + $friendlycaptcha_sitekey, + $friendlycaptcha_secret); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + $stream_context_create = $this->getFunctionMock("captcha", "stream_context_create"); + $stream_context_create->expects($this->once())->willReturn("stream_context_create"); + $file_get_contents = $this->getFunctionMock("captcha", "file_get_contents"); + $file_get_contents->expects($this->once())->willReturn($http_response); + + $_POST["frc-captcha-solution"] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('',$captcha, "unexpected return response during verify_captcha_challenge"); + } + + public function test_verify_captcha_challenge_badcaptcha(): void + { + + $friendlycaptcha_apiurl = 'http://127.0.0.1/'; + $friendlycaptcha_sitekey = 'FC12345'; + $friendlycaptcha_secret = 'secret'; + $http_response = '{"success": "false", "errors": {"0": "solution_invalid"}}'; + + $captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl, + $friendlycaptcha_sitekey, + $friendlycaptcha_secret); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + $stream_context_create = $this->getFunctionMock("captcha", "stream_context_create"); + $stream_context_create->expects($this->once())->willReturn("stream_context_create"); + $file_get_contents = $this->getFunctionMock("captcha", "file_get_contents"); + $file_get_contents->expects($this->once())->willReturn($http_response); + + $_POST["frc-captcha-solution"] = "ABCDE"; + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('badcaptcha',$captcha, "unexpected return response during verify_captcha_challenge"); + } + + public function test_verify_captcha_challenge_nocaptcha(): void + { + + $friendlycaptcha_apiurl = 'http://127.0.0.1/'; + $friendlycaptcha_sitekey = 'FC12345'; + $friendlycaptcha_secret = 'secret'; + + $captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl, + $friendlycaptcha_sitekey, + $friendlycaptcha_secret); + + $error_log = $this->getFunctionMock("captcha", "error_log"); + $error_log->expects($this->any())->willReturn(""); + + unset($_POST); + $captcha = $captchaInstance->verify_captcha_challenge(); + $this->assertEquals('captcharequired',$captcha, "unexpected return response during verify_captcha_challenge"); + } + +}