From 4bb39ed39be34dc34e740be932ae8e811614b999 Mon Sep 17 00:00:00 2001 From: Alejandro Ibarra Date: Fri, 5 Apr 2024 13:19:33 +0200 Subject: [PATCH] Add support for reCaptcha v3 --- Docs/Documentation/Configuration.md | 1 + config/users.php | 2 + src/View/Helper/UserHelper.php | 77 ++++++++++++++++--- templates/Users/login.php | 2 +- templates/Users/register.php | 2 +- tests/TestCase/View/Helper/UserHelperTest.php | 54 +++++++++++++ webroot/js/reCaptchaV3.js | 3 + 7 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 webroot/js/reCaptchaV3.js diff --git a/Docs/Documentation/Configuration.md b/Docs/Documentation/Configuration.md index 01b960aa4..64ee6a8b6 100644 --- a/Docs/Documentation/Configuration.md +++ b/Docs/Documentation/Configuration.md @@ -54,6 +54,7 @@ and add this to your config/users.php file: ```php 'Users.reCaptcha.key' => 'YOUR RECAPTCHA KEY', 'Users.reCaptcha.secret' => 'YOUR RECAPTCHA SECRET', +'Users.reCaptcha.version' => '2', //defaults to version 2 (backward compatibility) but you can use version 3 which is recommended 'Users.reCaptcha.registration' => true, //enable on registration 'Users.reCaptcha.login' => true, //enable on login ``` diff --git a/config/users.php b/config/users.php index 3f347e18e..41cb241f7 100644 --- a/config/users.php +++ b/config/users.php @@ -71,6 +71,8 @@ 'key' => null, // reCaptcha secret 'secret' => null, + // reCaptcha version. keep 2 for backward compatibility + 'version' => 2, // use reCaptcha in registration 'registration' => false, // use reCaptcha in login, valid values are false, true diff --git a/src/View/Helper/UserHelper.php b/src/View/Helper/UserHelper.php index 691ec0fce..b4bbf496f 100644 --- a/src/View/Helper/UserHelper.php +++ b/src/View/Helper/UserHelper.php @@ -18,6 +18,8 @@ use Cake\Utility\Inflector; use Cake\View\Helper; use CakeDC\Users\Utility\UsersUrl; +use Exception; +use InvalidArgumentException; /** * User helper @@ -42,7 +44,7 @@ class UserHelper extends Helper * @param array $options options * @return string */ - public function socialLogin($name, $options = []) + public function socialLogin(string $name, array $options = []): string { if (empty($options['label'])) { $options['label'] = __d('cake_d_c/users', 'Sign in with'); @@ -74,7 +76,7 @@ public function socialLogin($name, $options = []) * @param array $providerOptions Provider link options. * @return array Links to Social Login Urls */ - public function socialLoginList(array $providerOptions = []) + public function socialLoginList(array $providerOptions = []): array { if (!Configure::read('Users.Social.login')) { return []; @@ -105,7 +107,7 @@ public function socialLoginList(array $providerOptions = []) * @param array $options Array with option data. * @return string */ - public function logout($message = null, $options = []) + public function logout(?string $message = null, array $options = []): string { $url = UsersUrl::actionUrl('logout'); $title = empty($message) ? __d('cake_d_c/users', 'Logout') : $message; @@ -118,7 +120,7 @@ public function logout($message = null, $options = []) * * @return string|null */ - public function welcome() + public function welcome(): ?string { $identity = $this->getView()->getRequest()->getAttribute('identity'); if (!$identity) { @@ -143,7 +145,7 @@ public function welcome() * * @return void */ - public function addReCaptchaScript() + public function addReCaptchaScript(): void { $this->Html->script('https://www.google.com/recaptcha/api.js', [ 'block' => 'script', @@ -155,7 +157,7 @@ public function addReCaptchaScript() * * @return mixed */ - public function addReCaptcha() + public function addReCaptcha(): mixed { if (!Configure::read('Users.reCaptcha.key')) { return $this->Html->tag( @@ -167,10 +169,29 @@ public function addReCaptcha() ); } $this->addReCaptchaScript(); - try { - $this->Form->unlockField('g-recaptcha-response'); - } catch (\Exception $e) { + $version = Configure::read('Users.reCaptcha.version', 2); + $method = "addReCaptchaV$version"; + if (method_exists($this, $method)) { + try { + $this->Form->unlockField('g-recaptcha-response'); + } catch (Exception $e) { + } + + return $this->{$method}(); } + throw new InvalidArgumentException( + __d('cake_d_c/users', 'reCaptcha version is wrong. Please configure Users.reCaptcha.version as 2 or 3') + ); + } + + /** + * Add required element for reCaptcha v2 + * + * @return string + */ + private function addReCaptchaV2(): string + { + deprecationWarning('14.2.0', 'reCaptcha version 3 will be used as default in version 15.0.0'); return $this->Html->tag('div', '', [ 'class' => 'g-recaptcha', @@ -181,6 +202,38 @@ public function addReCaptcha() ]); } + /** + * Add required script for reCaptcha v3 + */ + private function addReCaptchaV3(): void + { + $this->Html->script('CakeDC/Users.reCaptchaV3', [ + 'block' => 'script', + ]); + } + + /** + * Add required options for reCaptcha v3 + * + * @param string $title + * @param array $options + * @return string + */ + public function button(string $title, array $options = []): string + { + $key = Configure::read('Users.reCaptcha.key'); + if ($key && Configure::read('Users.reCaptcha.version', 2) === 3) { + $options = array_merge($options, [ + 'class' => 'g-recaptcha', + 'data-sitekey' => $key, + 'data-callback' => 'onSubmit', + 'data-action' => 'submit', + ]); + } + + return $this->Form->button($title, $options); + } + /** * Generate a link if the target url is authorized for the logged in user * @@ -190,7 +243,7 @@ public function addReCaptcha() * @param array $options Array with option data. * @return string */ - public function link($title, $url = null, array $options = []) + public function link(string $title, array|string|null $url = null, array $options = []): string { trigger_error( 'UserHelper::link() deprecated since 3.2.1. Use AuthLinkHelper::link() instead', @@ -208,7 +261,7 @@ public function link($title, $url = null, array $options = []) * @param bool $isConnected User is connected with this provider * @return string */ - public function socialConnectLink($name, $provider, $isConnected = false) + public function socialConnectLink(string $name, array $provider, bool $isConnected = false): string { $optionClass = $provider['options']['class'] ?? null; $linkClass = 'btn btn-social btn-' . strtolower($name) . ($optionClass ? ' ' . $optionClass : ''); @@ -236,7 +289,7 @@ public function socialConnectLink($name, $provider, $isConnected = false) * @param array $socialAccounts All social accounts connected by a user. * @return string */ - public function socialConnectLinkList($socialAccounts = []) + public function socialConnectLinkList(array $socialAccounts = []): string { if (!Configure::read('Users.Social.login')) { return ''; diff --git a/templates/Users/login.php b/templates/Users/login.php index 99aaf4edd..2df2dab9d 100644 --- a/templates/Users/login.php +++ b/templates/Users/login.php @@ -45,6 +45,6 @@ ?> User->socialLoginList()); ?> - Form->button(__d('cake_d_c/users', 'Login')); ?> + User->button(__d('cake_d_c/users', 'Login')); ?> Form->end() ?> diff --git a/templates/Users/register.php b/templates/Users/register.php index e82673b67..1216b4599 100644 --- a/templates/Users/register.php +++ b/templates/Users/register.php @@ -35,6 +35,6 @@ } ?> - Form->button(__d('cake_d_c/users', 'Submit')) ?> + User->button(__d('cake_d_c/users', 'Submit')) ?> Form->end() ?> diff --git a/tests/TestCase/View/Helper/UserHelperTest.php b/tests/TestCase/View/Helper/UserHelperTest.php index a277ec50a..fb639330b 100644 --- a/tests/TestCase/View/Helper/UserHelperTest.php +++ b/tests/TestCase/View/Helper/UserHelperTest.php @@ -51,6 +51,16 @@ class UserHelperTest extends TestCase */ private $AuthLink; + /** + * @var (\Cake\View\View&\PHPUnit\Framework\MockObject\MockObject)|\PHPUnit\Framework\MockObject\MockObject + */ + private $View; + + /** + * @var ServerRequest + */ + private $request; + /** * setUp method * @@ -237,6 +247,50 @@ public function testAddReCaptcha() $this->assertEquals('
', $result); } + /** + * Test add ReCaptcha V3 + * + * @return void + */ + public function testAddReCaptchaV3() + { + $this->View->expects($this->exactly(2)) + ->method('append') + ->willReturnMap([ + ['https://www.google.com/recaptcha/api.js', null], + ['CakeDC/Users.reCaptchaV3', null], + ]); + Configure::write('Users.reCaptcha.key', 'testKey'); + Configure::write('Users.reCaptcha.version', 3); + Configure::write('Users.reCaptcha.theme', 'light'); + Configure::write('Users.reCaptcha.size', 'normal'); + Configure::write('Users.reCaptcha.tabindex', '3'); + $this->User->Form->create(); + $this->User->addReCaptcha(); + } + + public function testButton() + { + $title = 'test'; + $options = ['test' => 'test']; + $this->assertEquals($this->User->Form->button($title, $options), $this->User->button($title, $options)); + } + + public function testButtonReCaptchaV3() + { + Configure::write('Users.reCaptcha.key', 'testKey'); + Configure::write('Users.reCaptcha.version', 3); + $title = 'test'; + $options = ['test' => 'test']; + $reCaptchaOptions = [ + 'class' => 'g-recaptcha', + 'data-sitekey' => 'testKey', + 'data-callback' => 'onSubmit', + 'data-action' => 'submit', + ]; + $this->assertEquals($this->User->Form->button($title, array_merge($options, $reCaptchaOptions)), $this->User->button($title, $options)); + } + /** * Test add ReCaptcha field * diff --git a/webroot/js/reCaptchaV3.js b/webroot/js/reCaptchaV3.js new file mode 100644 index 000000000..42d4d65e7 --- /dev/null +++ b/webroot/js/reCaptchaV3.js @@ -0,0 +1,3 @@ +function onSubmit(token) { + document.forms[0].submit() +}