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..860508b0a 100644 --- a/config/users.php +++ b/config/users.php @@ -71,11 +71,17 @@ '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 'login' => false, ], + 'passwordMeter' => [ + 'enabled' => true, + 'requiredScore' => 3, + ], 'Tos' => [ // determines if the user should include tos accepted 'required' => true, diff --git a/src/View/Helper/UserHelper.php b/src/View/Helper/UserHelper.php index 691ec0fce..dd3d500e1 100644 --- a/src/View/Helper/UserHelper.php +++ b/src/View/Helper/UserHelper.php @@ -18,6 +18,7 @@ use Cake\Utility\Inflector; use Cake\View\Helper; use CakeDC\Users\Utility\UsersUrl; +use InvalidArgumentException; /** * User helper @@ -42,7 +43,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 +75,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 +106,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 +119,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,19 +144,42 @@ public function welcome() * * @return void */ - public function addReCaptchaScript() + public function addReCaptchaScript(): void { $this->Html->script('https://www.google.com/recaptcha/api.js', [ 'block' => 'script', ]); } + /** + * @return void + */ + public function addPasswordMeterStript(): void + { + $this->Html->script('CakeDC/Users.pswmeter', [ + 'block' => 'script', + ]); + } + + /** + * @return string + */ + public function addPasswordMeter(): string + { + $this->addPasswordMeterStript(); + $requiredScore = Configure::read('Users.passwordMeter.requiredScore', 3); + $script = $this->Html->scriptBlock("const requiredScore = $requiredScore", ['defer' => true]); + + return $this->Html->tag('div', '', ['id' => 'pswmeter']) . + $this->Html->tag('div', '', ['id' => 'pswmeter-message']) . $script; + } + /** * Add reCaptcha to the form * * @return mixed */ - public function addReCaptcha() + public function addReCaptcha(): mixed { if (!Configure::read('Users.reCaptcha.key')) { return $this->Html->tag( @@ -167,10 +191,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 +224,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 +265,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 +283,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 +311,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/change_password.php b/templates/Users/change_password.php index 7efba8f10..c266c3aa3 100644 --- a/templates/Users/change_password.php +++ b/templates/Users/change_password.php @@ -13,8 +13,12 @@ = $this->Form->control('password', [ 'type' => 'password', 'required' => true, + 'id' => 'new-password', 'label' => __d('cake_d_c/users', 'New password')]); ?> + + = $this->User->addPasswordMeter() ?> + = $this->Form->control('password_confirm', [ 'type' => 'password', 'required' => true, @@ -22,6 +26,6 @@ ?> - = $this->Form->button(__d('cake_d_c/users', 'Submit')); ?> + = $this->Form->button(__d('cake_d_c/users', 'Submit'), ['id' => 'btn-submit']); ?> = $this->Form->end() ?> - \ No newline at end of file + 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 @@ ?> = implode(' ', $this->User->socialLoginList()); ?> - = $this->Form->button(__d('cake_d_c/users', 'Login')); ?> + = $this->User->button(__d('cake_d_c/users', 'Login')); ?> = $this->Form->end() ?> diff --git a/templates/Users/profile.php b/templates/Users/profile.php index aa4759fb7..30aa97094 100644 --- a/templates/Users/profile.php +++ b/templates/Users/profile.php @@ -32,7 +32,7 @@
= h($user->username) ?>
= h($user->email) ?>
- = $this->User->socialConnectLinkList($user->social_accounts) ?> + = $this->User->socialConnectLinkList($user->social_accounts ?? []) ?> social_accounts)): ?> diff --git a/templates/Users/register.php b/templates/Users/register.php index e82673b67..cad1cc40a 100644 --- a/templates/Users/register.php +++ b/templates/Users/register.php @@ -19,7 +19,10 @@ Form->control('username', ['label' => __d('cake_d_c/users', 'Username')]); echo $this->Form->control('email', ['label' => __d('cake_d_c/users', 'Email')]); - echo $this->Form->control('password', ['label' => __d('cake_d_c/users', 'Password')]); + echo $this->Form->control('password', ['label' => __d('cake_d_c/users', 'Password'), 'id' => 'new-password']); + if (Configure::read('Users.passwordMeter')) { + echo $this->User->addPasswordMeter(); + } echo $this->Form->control('password_confirm', [ 'required' => true, 'type' => 'password', @@ -35,6 +38,6 @@ } ?> - = $this->Form->button(__d('cake_d_c/users', 'Submit')) ?> + = $this->User->button(__d('cake_d_c/users', 'Submit'), ['id' => 'btn-submit']) ?> = $this->Form->end() ?> diff --git a/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php index f7b082f13..6ddf61e93 100644 --- a/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/PasswordManagementTraitIntegrationTest.php @@ -80,7 +80,7 @@ public function testRequestResetPasswordPostValidEmail() $this->assertResponseContains('Please enter the new password'); $this->assertResponseContains('assertResponseContains('assertResponseContains(''); + $this->assertResponseContains(''); $this->post('/users/change-password', [ 'password' => '9080706050', diff --git a/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php index afa4a15bb..5e27ef5ea 100644 --- a/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/RegisterTraitIntegrationTest.php @@ -43,13 +43,13 @@ public function testRegister() $this->assertResponseContains(''); $this->assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); } /** @@ -78,13 +78,13 @@ public function testRegisterPostWithErrors() $this->assertResponseContains(''); $this->assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains('assertResponseContains(''); - $this->assertResponseContains(''); + $this->assertResponseContains(''); } /** diff --git a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php index 6965f9c6a..9a35d74b3 100644 --- a/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php +++ b/tests/TestCase/Controller/Traits/Integration/SimpleCrudTraitIntegrationTest.php @@ -83,7 +83,7 @@ public function testCrud() $this->assertResponseContains('