Skip to content

Commit

Permalink
Merge pull request #8 from passioneight/dev
Browse files Browse the repository at this point in the history
improvement: re-implement the bundle
  • Loading branch information
passioneight committed Sep 19, 2022
2 parents a7256c1 + a3ba6f1 commit 7b1ecd5
Show file tree
Hide file tree
Showing 39 changed files with 514 additions and 376 deletions.
15 changes: 11 additions & 4 deletions README.md
Expand Up @@ -8,15 +8,22 @@ Google provides a useful _CAPTCHA_ that can be used to provide better security f
- [Installation](/documentation/10_installation.md)
- [Configuration](/documentation/20_configuration.md)
- [Usage](/documentation/30_usage.md)
- [Including the Script](/documentation/30_usage.md#including-the-script)
- [Implementing the Form](/documentation/30_usage.md#implementing-the-form)
- [Explicit Token Validation](/documentation/30_usage.md#explicit-token-validation)
- [Extending the Bundle](/documentation/40_extending_the_bundle.md)

# When should I use this bundle?
Whenever an action in your **Pimcore**-project needs to be protected with [Google reCAPTCHA V3](https://www.google.com/recaptcha/intro/v3.html).

> V3 is the only supported version.
# Why should I use this bundle?
If your site has some kind of authentication (e.g., login, registration, ...) or allows to submit product reviews, you'll
want to include [Google reCAPTCHA](https://www.google.com/recaptcha/intro/v3.html) into your site as this will increase both _trustworthiness_ and _security_.
[See Google's explanatory video for details](https://youtu.be/tbvxFW4UJdU).
To protect your site against bots. For example, if your site has some kind of authentication (e.g., login, registration, ...),
you'll want to include [Google reCAPTCHA](https://www.google.com/recaptcha/intro/v3.html) into your site as this will
increase _security_. [See Google's explanatory video for details](https://youtu.be/tbvxFW4UJdU).

> Please note that there are some **privacy concerns**, as it is said (though, not proven) that Google may collect user data
> for their own benefits - other than Google reCAPTCHA improvements.
> for their own benefits - other than Google reCAPTCHA improvements.
>
>**Beware that you are responsible for GDPR compliance.**
17 changes: 17 additions & 0 deletions UPGRADENOTES.md
@@ -0,0 +1,17 @@
# Upgrade Notes
## 2.0.0
- Namespace changed from `Passioneight\Bundle\PimcoreGoogleRecaptchaBundle` to
`Passioneight\PimcoreGoogleRecaptcha`. Use your IDE to replace namespaces.

- Requirements changed to `pimcore/pimcore ^10.5`, which means PHP 8 is used.

- `google-recaptcha.js` is now a class. You'll have to transpile the file. For example,
you may want to use [Webpack Encore](https://github.com/symfony/webpack-encore) (see docs).

- `google-recaptcha.js` does not load Google's script by default anymore. Instead, call the `loadScript` method explicitly.
This ensures GDPR compliance, unless you call the method without user consent.

- Added some `Trait`s to ease dependency injection of services.

## 1.0.0
No upgrade notes available, because `v1.0.0` was the first version.
10 changes: 8 additions & 2 deletions composer.json
Expand Up @@ -4,13 +4,19 @@
"license": "GPL-3.0",
"autoload": {
"psr-4": {
"Passioneight\\Bundle\\PimcoreGoogleRecaptchaBundle\\": "src/"
"Passioneight\\PimcoreGoogleRecaptcha\\": "src/"
}
},

"require": {
"pimcore/pimcore": "^10.5",
"passioneight/php-utilities": "^2.0"
},

"extra": {
"pimcore": {
"bundles": [
"Passioneight\\Bundle\\PimcoreGoogleRecaptchaBundle\\PimcoreGoogleRecaptchaBundle"
"Passioneight\\PimcoreGoogleRecaptcha\\PimcoreGoogleRecaptchaBundle"
]
}
}
Expand Down
6 changes: 3 additions & 3 deletions documentation/05_prerequisites.md
@@ -1,5 +1,5 @@
# Installation
To use Google reCAPTCHA and, therefore, this bundle, you'll need to
[register a reCAPTCHA-key](https://www.google.com/recaptcha/admin/create).
Google requires you to [register a reCAPTCHA-key](https://www.google.com/recaptcha/admin/create). The reCAPTCHA-key
will also be needed as part of the configuration of this bundle.

### [Next Chapter: Installation](/documentation/10_installation.md)
### [Next Chapter: Installation](/documentation/10_installation.md)
9 changes: 4 additions & 5 deletions documentation/10_installation.md
@@ -1,10 +1,9 @@
# Installation

Execute the following commands, and you are ready to go:
Execute the following commands to install and enable this bundle:

```
COMPOSER_MEMORY_LIMIT=-1 composer require passioneight/pimcore-google-recaptcha
php bin/console pimcore:bundle:enable PimcoreGoogleRecaptchaBundle
composer require passioneight/pimcore-google-recaptcha
bin/console pimcore:bundle:enable PimcoreGoogleRecaptchaBundle
```

### [Next Chapter: Configuration](/documentation/20_configuration.md)
### [Next Chapter: Configuration](/documentation/20_configuration.md)
3 changes: 2 additions & 1 deletion documentation/20_configuration.md
Expand Up @@ -6,8 +6,9 @@ pimcore_google_recaptcha:
public_key: '<public-key>'
private_key: '<private-key>'
```

> If you need a different **score-threshold** for your site, you can set the corresponding config.
>
> Have a look at the complete configuration options [here](/src/Resources/config/config.example.yml).
### [Next Chapter: Usage](/documentation/30_usage.md)
### [Next Chapter: Usage](/documentation/30_usage.md)
118 changes: 69 additions & 49 deletions documentation/30_usage.md
@@ -1,66 +1,86 @@
# Usage
After configuring the bundle, you'll need some JavaScript in your HTML-Code.

> Note that the `recpatcha.js` only supports **V3**.
```html
<script>
var _config = _config || {};
_config.googleRecaptcha = {
debug: {{ google_recaptcha_debug() }},
publicKey: "{{ google_recaptcha_public_key() }}",
querySelector: ".google-recaptcha",
defaultAction: "{{ google_recaptcha_default_action() }}",
};
</script>
<script src="{{ asset('bundles/googlerecaptcha/js/recaptcha.js') }}"></script>
After [configuring the bundle]((/documentation/20_configuration.md)), the JavaScript needs to become aware of the bundle's
configuration. Include the following template to achieve this:

```twig
{% include '@PimcoreGoogleRecaptcha/google-recaptcha.html.twig' %}
```

> No need to change the JavaScript, as you can simply change the bundle's configuration to alter the `_config` variable.
> You can also pass a `nonce` parameter to the template to improve security
> (see [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) for more information).
Next, add a hidden form field to your form, like so:
```html
<input type="hidden" name="google-recaptcha" class="google-recaptcha" />
```
### Including the Script
Import the script in your main script (e.g., `app.js`) and call the `loadScript` method once you have the user's
consent to load Google reCAPTCHA:

> If you want to customize the `action` that is sent to Google, add a `data-action="myCustomAction"` attribute to the form field.
```js
let recaptcha = new PassioneightGoogleRecaptcha()

The script will automatically detect any elements with the `google-recaptcha` class and try to find a parent form.
If a parent form was found, _any_ corresponding submit buttons (i.e., `<input type="submit" ...>`) will be used to trigger
Google's `grecaptcha.execute` method before the form is actually submitted.
// Omitted: asking for the user's permission
let userConsent = true

> The reCAPTCHA is executed on each click, since Google states: _reCAPTCHA tokens expire after two minutes. If you're
> protecting an action with reCAPTCHA, make sure to call execute when the user takes the action_.
if(userConsent) {
recaptcha.loadScript()
}
```

Once the [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolves,
a token will be added to the hidden input field. Afterwards the form will be submitted.
> You may want to use a custom callback when the `grecaptcha` is ready. You can pass the callback to the `loadScript`
> method, like so: `recaptcha.loadScript(() => { recaptcha.fetchRecaptchaToken(event) })`.
Finally, you'll need to verify that the user is human. Various, extendable services are provided for this:
- `TokenDecoderInterface`
- `ResponseParserInterface`
- `ResponseValidatorInterface`
The script will automatically detect any form fields with the defined `querySelector` (i.e., `.google-recaptcha` by default)
and try to find a parent form. If the user submits the form, the reCAPTCHA-token is loaded first, so it is submitted too.

Inject both the `TokenDecoderInterface` and `ResponseValidatorInterface` into your class, which handles the form submission
and add the following code:
```php
$recaptchaToken = "theSubmittedToken";
$recaptchaResponse = $this->responseDecoder->decodeToken($recaptchaToken);
$this->responseValidator->validate($recaptchaResponse); // Throws an exception if invalid
```
> To avoid timeouts, the reCAPTCHA is executed **on click**, since Google states: _reCAPTCHA tokens expire after two
> minutes. If you're protecting an action with reCAPTCHA, make sure to call execute when the user takes the action_.
Alternatively, you can trigger a `ValidationEvent` using Symfony's `EventDispatcherInterface`. If the token is invalid,
a `ValidationException` is thrown.
### Implementing the Form
While the script is loaded, it won't find any forms to protect just yet. You'll need to add a hidden form field, which
will be used to submit the token that was loaded from Google.

Conveniently, you can add this field as follows:
```php
try{
$this->eventDispacther->dispatch(new ValidationEvent("theSubmittedToken"));
} catch (ValidationException $e) {
// Do some error handling

namespace App\Form\Authentication;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Passioneight\PimcoreGoogleRecaptcha\Form\Field\GoogleRecaptchaField;

class RegistrationForm extends AbstractType
{
const FIELD_GOOGLE_RECAPTCHA = "google-recaptcha";

/**
* @inheritDoc
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);

// Omitted: any other fields of the form

$builder->add(self::FIELD_GOOGLE_RECAPTCHA, GoogleRecaptchaField::class);

$builder->add('submit', SubmitType::class, [
'label_format' => 'form.registration.submit'
]);
}
}
```

> You may want to checkout our [Form Builder Bundle](https://github.com/passioneight/form-builder) for an even easier integration.
> You'll only have to include the JS and add a `GoogleRecaptcha` field to your form.
> If you want to customize the `action` that is sent to Google, add a `data-action="myCustomAction"` to the `SubmitType`.
Using the `GoogleRecaptchaField`, the token will be validated automatically, because it contains the `GoogleRecaptchaConstraint`.

### Explicit Token Validation
If you want to validate the token yourself, dispatch a `ValidationEvent`:
```php
try{
$this->eventDispacther->dispatch(new ValidationEvent($token));
} catch (ValidationException $e) {
// Do some error handling
}
```

### [Next Chapter: Extending the bundle](/documentation/40_extending_the_bundle.md)
### [Next Chapter: Extending the bundle](/documentation/40_extending_the_bundle.md)
16 changes: 8 additions & 8 deletions documentation/40_extending_the_bundle.md
@@ -1,18 +1,18 @@
# Extending the Bundle
If, for some reason, the provided code is not sufficient, you may extend the provided services like so:
If the provided code is not sufficient for your use case, you can extend the provided services like so:

```yaml
Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Decoder\TokenDecoderInterface:
Passioneight\PimcoreGoogleRecaptcha\Service\Decoder\TokenDecoderInterface:
class: AppBundle\Service\GoogleRecaptcha\Decoder\TokenDecoder

Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Parser\ResponseParserInterface:
Passioneight\PimcoreGoogleRecaptcha\Service\Parser\ResponseParserInterface:
class: AppBundle\Service\GoogleRecaptcha\Parser\ResponseParser

Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Validator\ResponseValidatorInterface:
Passioneight\PimcoreGoogleRecaptcha\Service\Validator\ResponseValidatorInterface:
class: AppBundle\Service\GoogleRecaptcha\Validator\ResponseValidator
```

> Your custom classes should extend from the provided abstract classes:
> - `Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Decoder\AbstractTokenDecoder`
> - `Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Parser\AbstractResponseParser`
> - `Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Service\Validator\AbstractResponseValidator`
> Consider extending the following classes instead of starting from scratch:
> - `Passioneight\PimcoreGoogleRecaptcha\Service\Decoder\AbstractTokenDecoder`
> - `Passioneight\PimcoreGoogleRecaptcha\Service\Parser\AbstractResponseParser`
> - `Passioneight\PimcoreGoogleRecaptcha\Service\Validator\AbstractResponseValidator`
2 changes: 1 addition & 1 deletion src/Constant/Configuration.php
@@ -1,6 +1,6 @@
<?php

namespace Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant;
namespace Passioneight\PimcoreGoogleRecaptcha\Constant;

class Configuration
{
Expand Down
2 changes: 1 addition & 1 deletion src/Constant/DefaultConfigurationValue.php
@@ -1,6 +1,6 @@
<?php

namespace Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant;
namespace Passioneight\PimcoreGoogleRecaptcha\Constant;

class DefaultConfigurationValue
{
Expand Down
2 changes: 1 addition & 1 deletion src/Constant/ResponseKey.php
@@ -1,6 +1,6 @@
<?php

namespace Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant;
namespace Passioneight\PimcoreGoogleRecaptcha\Constant;

class ResponseKey
{
Expand Down
21 changes: 6 additions & 15 deletions src/DependencyInjection/Configuration.php
@@ -1,9 +1,9 @@
<?php

namespace Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\DependencyInjection;
namespace Passioneight\PimcoreGoogleRecaptcha\DependencyInjection;

use Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant\Configuration as Config;
use Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant\DefaultConfigurationValue;
use Passioneight\PimcoreGoogleRecaptcha\Constant\Configuration as Config;
use Passioneight\PimcoreGoogleRecaptcha\Constant\DefaultConfigurationValue;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
Expand All @@ -13,7 +13,7 @@ class Configuration implements ConfigurationInterface
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder(Config::ROOT);
$rootNode = $treeBuilder->getRootNode();
Expand All @@ -25,25 +25,19 @@ public function getConfigTreeBuilder()
return $treeBuilder;
}

/**
* @param ArrayNodeDefinition $rootNode
*/
private function addGoogleRecaptchaKeyConfiguration(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->scalarNode(Config::PUBLIC_KEY)
->defaultNull()
->isRequired()
->end()
->scalarNode(Config::PRIVATE_KEY)
->defaultNull()
->isRequired()
->end()
->end();
}

/**
* @param ArrayNodeDefinition $rootNode
*/
private function addGoogleRecaptchaValidationConfiguration(ArrayNodeDefinition $rootNode)
{
$rootNode
Expand All @@ -54,9 +48,6 @@ private function addGoogleRecaptchaValidationConfiguration(ArrayNodeDefinition $
->end();
}

/**
* @param ArrayNodeDefinition $rootNode
*/
private function addMiscConfiguration(ArrayNodeDefinition $rootNode)
{
$rootNode
Expand Down
17 changes: 7 additions & 10 deletions src/DependencyInjection/PimcoreGoogleRecaptchaExtension.php
@@ -1,8 +1,9 @@
<?php

namespace Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\DependencyInjection;
namespace Passioneight\PimcoreGoogleRecaptcha\DependencyInjection;

use Passioneight\Bundle\PimcoreGoogleRecaptchaBundle\Constant\Configuration as Config;
use Passioneight\Bundle\PhpUtilitiesBundle\Service\Utility\MethodUtility;
use Passioneight\PimcoreGoogleRecaptcha\Service\Configuration\GoogleRecaptchaConfiguration;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
Expand All @@ -16,19 +17,15 @@ class PimcoreGoogleRecaptchaExtension extends ConfigurableExtension
*/
protected function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('config.yml');

$this->populateContainer($mergedConfig, $container);
}

/**
* Populates the container in order to access the configuration later on, if needed.
* @param array $config
* @param ContainerBuilder $container
*/
private function populateContainer(array $config, ContainerBuilder $container)
private function populateContainer(array $config, ContainerBuilder $container): void
{
$container->setParameter(Config::ROOT, $config);
$serviceDefinition = $container->getDefinition(GoogleRecaptchaConfiguration::class);
$serviceDefinition->addMethodCall(MethodUtility::createSetter("configuration"), [$config]);
}
}

0 comments on commit 7b1ecd5

Please sign in to comment.