Skip to content

Commit

Permalink
add the captcha module friendlycaptcha (#895)
Browse files Browse the repository at this point in the history
  • Loading branch information
David Coutadeur committed Jul 3, 2024
1 parent bcbc71f commit f68b2ae
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 2 deletions.
7 changes: 6 additions & 1 deletion conf/config.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion docs/config_general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.friendlycaptcha.com/>`_ for more information

You can also integrate any other Captcha module by developping the corresponding plugin. (see :doc:`developpers` )


.. |image0| image:: images/br.png
Expand Down
114 changes: 114 additions & 0 deletions lib/captcha/FriendlyCaptcha.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php namespace captcha;

include_once( __DIR__ . "/Captcha.php");
require_once(__DIR__."/../../vendor/autoload.php");


class FriendlyCaptcha
{

private $friendlycaptcha_apiurl;
private $friendlycaptcha_sitekey;
private $friendlycaptcha_secret;

public function __construct($friendlycaptcha_apiurl, $friendlycaptcha_sitekey, $friendlycaptcha_secret)
{
$this->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 = '
<script
type="module"
src="https://cdn.jsdelivr.net/npm/[email protected]/widget.module.min.js"
async
defer
></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/[email protected]/widget.min.js" async defer></script>
';

return $captcha_js;
}

# Function that generate the html part containing the captcha
function generate_html_captcha($messages){

$captcha_html ='
<div class="row mb-3">
<div class="col-sm-4 col-form-label text-end captcha">
</div>
<div class="col-sm-8">
<div class="frc-captcha" data-sitekey="'.$this->friendlycaptcha_sitekey.'"></div>
</div>
</div>';

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;
}

}


?>
123 changes: 123 additions & 0 deletions tests/FriendlyCaptchaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/captcha/FriendlyCaptcha.php';

class FriendlyCaptchaTest extends \PHPUnit\Framework\TestCase
{

use \phpmock\phpunit\PHPMock;

public function test_construct(): void
{
$friendlycaptcha_apiurl = 'http://127.0.0.1/';
$friendlycaptcha_sitekey = 'FC12345';
$friendlycaptcha_secret = 'secret';

$captchaInstance = new captcha\FriendlyCaptcha($friendlycaptcha_apiurl,
$friendlycaptcha_sitekey,
$friendlycaptcha_secret);

$this->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('/<div class="frc-captcha" data-sitekey="'.$friendlycaptcha_sitekey.'">/',$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");
}

}

0 comments on commit f68b2ae

Please sign in to comment.