Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Allow developers to handle form submission separately. #76

Merged
merged 1 commit into from
Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ UndefinedOffset\NoCaptcha\Forms\NocaptchaField:
default_type: "image" #Default captcha type (optional, image or audio, defaults to image)
default_size: "normal" #Default size (optional, normal, compact or invisible, defaults to normal)
default_badge: "bottomright" #Default badge position (bottomright, bottomleft or inline, defaults to bottomright)
default_handle_submit: true #Default setting for whether nocaptcha should handle form submission. See "Handling form submission" below.
proxy_server: "" #Your proxy server address (optional)
proxy_port: "" #Your proxy server address port (optional)
proxy_auth: "" #Your proxy server authentication information (optional)
Expand Down Expand Up @@ -124,6 +125,50 @@ $captchaField->setMinimumScore(0.2);
For more information about version 3, including how to implement custom actions
see https://developers.google.com/recaptcha/docs/v3

## Handling form submission
By default, the javascript included with this module will add a submit event handler to your form.

If you need to handle form submissions in a special way (for example to support front-end validation),
you can choose to handle form submit events yourself.

This can be configured site-wide using the Config API
```yml
UndefinedOffset\NoCaptcha\Forms\NocaptchaField:
default_handle_submit: false
```

Or on a per form basis:
```php
$captchaField = $form->Fields()->fieldByName('Captcha');
$captchaField->setHandleSubmitEvents(false);
```

With this configuration no event handlers will be added by this module to your form. Instead, a
function will be provided called `nocaptcha_handleCaptcha` which you can call from your code
when you're ready to submit your form. It has the following signature:
```js
function nocaptcha_handleCaptcha(form, callback)
```
`form` must be the form element, and `callback` should be a function that finally submits the form,
though it is optional.

In the simplest case, you can use it like this:
```js
document.addEventListener("DOMContentLoaded", function(event) {
// where formID is the element ID for your form
const form = document.getElementById(formID);
const submitListener = function(event) {
event.preventDefault();
let valid = true;
/* Your validation logic here */
if (valid) {
nocaptcha_handleCaptcha(form, form.submit.bind(form));
}
};
form.addEventListener('submit', submitListener);
});
```

## Reporting an issue

When you're reporting an issue please ensure you specify what version of
Expand Down
27 changes: 27 additions & 0 deletions javascript/NocaptchaField_noHandler_v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var _noCaptchaFields=_noCaptchaFields || [];

// form isn't necessary for v2, but we have it here to keep the function signature consistent.
function nocaptcha_handleCaptcha(form, callback) {
if (callback) {
callback();
}
};

function noCaptchaFieldRender() {
var render = function(field) {
var options={
'sitekey': field.getAttribute('data-sitekey'),
'theme': field.getAttribute('data-theme'),
'type': field.getAttribute('data-type'),
'size': field.getAttribute('data-size'),
'badge': field.getAttribute('data-badge'),
};

var widget_id = grecaptcha.render(field, options);
field.setAttribute("data-widgetid", widget_id);
}

for(var i=0;i<_noCaptchaFields.length;i++) {
render(document.getElementById('Nocaptcha-'+_noCaptchaFields[i]));
}
}
9 changes: 9 additions & 0 deletions javascript/NocaptchaField_noHandler_v3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function nocaptcha_handleCaptcha(form, callback) {
var input = document.getElementById('Nocaptcha-' + form.getAttribute('id'));
grecaptcha.execute(input.getAttribute('data-sitekey'), {action: 'submit'}).then(function (token) {
input.value = token;
if (callback) {
callback();
}
});
};
99 changes: 82 additions & 17 deletions src/Forms/NocaptchaField.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ class NocaptchaField extends FormField {
*/
private static $default_size='normal';

/**
* Whether form submit events are handled directly by this module.
* If false, a function is provided that can be called by user code submit handlers.
* @var boolean
* @default true
*/
private static $default_handle_submit = true;

/**
* Recaptcha Site Key
* Configurable via Injector config
Expand Down Expand Up @@ -150,6 +158,13 @@ class NocaptchaField extends FormField {
*/
protected $minimumScore;

/**
* Whether form submit events are handled directly by this module.
* If false, a function is provided that can be called by user code submit handlers.
* @var boolean
*/
private $handleSubmitEvents;

/**
* Creates a new Recaptcha 2 field.
* @param string $name The internal field name, passed to forms.
Expand All @@ -165,6 +180,7 @@ public function __construct($name, $title=null, $value=null) {
$this->_captchaType=self::config()->default_type;
$this->_captchaSize=self::config()->default_size;
$this->_captchaBadge=self::config()->default_badge;
$this->handleSubmitEvents = self::config()->default_handle_submit;
}

/**
Expand All @@ -180,44 +196,71 @@ public function Field($properties=array()) {
user_error('You must configure Nocaptcha.site_key and Nocaptcha.secret_key, you can retrieve these at https://google.com/recaptcha', E_USER_ERROR);
}

$form = $this->getForm();

if ($this->config()->get('recaptcha_version') == 2) {
$exemptActionsString = implode("' , '", $form->getValidationExemptActions());
$this->configureRequirementsForV2();
} else {
$this->configureRequirementsForV3();
}

return parent::Field($properties);
}

/**
* Configure any javascript and css requirements that are specific for recaptcha v2.
*/
protected function configureRequirementsForV2()
{
Requirements::customScript(
"(function() {\n" .
"var gr = document.createElement('script'); gr.type = 'text/javascript'; gr.async = true;\n" .
"gr.src = ('https:' == document.location.protocol ? 'https://www' : 'http://www') + " .
"'.google.com/recaptcha/api.js?render=explicit&hl=" .
Locale::getPrimaryLanguage(i18n::get_locale()) .
"&onload=noCaptchaFieldRender';\n" .
"var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gr, s);\n" .
"})();\n",
'NocaptchaField-lib'
);
if ($this->getHandleSubmitEvents()) {
$exemptActionsString = implode("' , '", $this->getForm()->getValidationExemptActions());
Requirements::javascript('undefinedoffset/silverstripe-nocaptcha:javascript/NocaptchaField.js');
Requirements::customScript(
"var _noCaptchaFields=_noCaptchaFields || [];_noCaptchaFields.push('".$this->ID()."');" .
"var _noCaptchaValidationExemptActions=_noCaptchaValidationExemptActions || [];" .
"_noCaptchaValidationExemptActions.push('" . $exemptActionsString . "');",
"NocaptchaField-" . $this->ID()
);
} else {
Requirements::customScript(
"(function() {\n" .
"var gr = document.createElement('script'); gr.type = 'text/javascript'; gr.async = true;\n" .
"gr.src = ('https:' == document.location.protocol ? 'https://www' : 'http://www') + " .
"'.google.com/recaptcha/api.js?render=explicit&hl=" .
Locale::getPrimaryLanguage(i18n::get_locale()) .
"&onload=noCaptchaFieldRender';\n" .
"var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gr, s);\n" .
"})();\n",
'NocaptchaField-lib'
"var _noCaptchaFields=_noCaptchaFields || [];_noCaptchaFields.push('".$this->ID()."');",
"NocaptchaField-" . $this->ID()
);
} else {
Requirements::javascript('https://www.google.com/recaptcha/api.js?render=' . urlencode($siteKey) . '&onload=noCaptchaFormRender');
Requirements::javascript('undefinedoffset/silverstripe-nocaptcha:javascript/NocaptchaField_noHandler_v2.js');
}
}

/**
* Configure any javascript and css requirements that are specific for recaptcha v3.
*/
protected function configureRequirementsForV3()
{
Requirements::customCSS('.nocaptcha { display: none !important; }', self::class);
if ($this->getHandleSubmitEvents()) {
Requirements::javascript('https://www.google.com/recaptcha/api.js?render=' . urlencode($this->getSiteKey()) . '&onload=noCaptchaFormRender');
Requirements::javascript('undefinedoffset/silverstripe-nocaptcha:javascript/NocaptchaField_v3.js');
Requirements::customCSS('.nocaptcha { display: none !important; }', self::class);

$form = $this->getForm();
$helper = $form->getTemplateHelper();
$id = $helper->generateFormID($form);

Requirements::customScript(
"var _noCaptchaForms=_noCaptchaForms || [];_noCaptchaForms.push('". $id . "');",
'NocaptchaForm-' . $id
);
} else {
Requirements::javascript('https://www.google.com/recaptcha/api.js?render=' . urlencode($this->getSiteKey()));
Requirements::javascript('undefinedoffset/silverstripe-nocaptcha:javascript/NocaptchaField_noHandler_v3.js');
}

return parent::Field($properties);
}

/**
Expand Down Expand Up @@ -294,6 +337,28 @@ public function validate($validator) {
return true;
}

/**
* Sets whether form submit events are handled directly by this module.
*
* @param boolean $value
* @return NocaptchaField
*/
public function setHandleSubmitEvents(bool $value)
{
$this->handleSubmitEvents = $value;
return $this;
}

/**
* Get whether form submit events are handled directly by this module.
*
* @return boolean
*/
public function getHandleSubmitEvents(): bool
{
return $this->handleSubmitEvents;
}

/**
* Sets the theme for this captcha
* @param string $value Theme to set it to, currently the api supports light and dark
Expand Down