[IMP] google_recaptcha, *: integrate recaptchaV3

* = base_setup, website_form, website_sale, website_crm,
crm_iap_lead_website, website_hr_recruitment, website_mass_mailing

Integrate reCaptchaV3 on website_form submit and website_mass_mailing
subscription.
You can now use ReCaptchaV3 to add reCaptcha verification in any module
using google_recaptcha.

Also added a better error management on the form with custom messages.

task-2217980

closes odoo/odoo#48466

Related: odoo/enterprise#9649
Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
This commit is contained in:
fja-odoo
2020-06-05 10:58:50 +00:00
parent fea1706f27
commit 5411486134
27 changed files with 468 additions and 51 deletions
@@ -30,6 +30,7 @@ class ResConfigSettings(models.TransientModel):
module_web_unsplash = fields.Boolean("Unsplash Image Library")
module_partner_autocomplete = fields.Boolean("Partner Autocomplete")
module_base_geolocalize = fields.Boolean("GeoLocalize")
module_google_recaptcha = fields.Boolean("reCAPTCHA: Easy on Humans, Hard on Bots")
report_footer = fields.Text(related="company_id.report_footer", string='Custom Report Footer', help="Footer text displayed at the bottom of all reports.", readonly=False)
group_multi_currency = fields.Boolean(string='Multi-Currencies',
implied_group='base.group_multi_currency',
@@ -354,6 +354,20 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="recaptcha">
<div class="o_setting_left_pane">
<field name="module_google_recaptcha"/>
</div>
<div class="o_setting_right_pane" id="website_recaptcha_settings">
<label for="module_google_recaptcha"/>
<div class="text-muted">
reCAPTCHA is a free service that protects your website from spam and abuse.
</div>
<div class="content-group" attrs="{'invisible': [('module_google_recaptcha', '=', False)]}" id="recaptcha_warning">
<div class="mt16 text-warning"><strong>Save</strong> this page and come back here to set up reCaptcha.</div>
</div>
</div>
</div>
</div>
<widget name='res_config_dev_tool'/>
<div id='about'>
@@ -8,12 +8,11 @@ from odoo.http import request
class ContactController(WebsiteForm):
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
def website_form(self, model_name, **kwargs):
def _handle_website_form(self, model_name, **kwargs):
if model_name == 'crm.lead':
# Add the ip_address to the request in order to add this to the lead
# that will be created. With this, we avoid to create a lead from
# reveal if a lead is already created from the contact form.
request.params['reveal_ip'] = request.httprequest.remote_addr
return super(ContactController, self).website_form(model_name, **kwargs)
return super(ContactController, self)._handle_website_form(model_name, **kwargs)
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+17
View File
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Google reCAPTCHA integration',
'category': 'Hidden',
'version': '1.0',
'description': """
This module implements reCaptchaV3 so that you can prevent bot spam on your public modules.
""",
'depends': ['base_setup'],
'data': [
'views/assets.xml',
'views/res_config_settings_view.xml',
],
'auto_install': True,
}
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_http
from . import res_config_settings
+96
View File
@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import requests
from odoo import api, models, _
from odoo.http import request
from odoo.exceptions import UserError, ValidationError
logger = logging.getLogger(__name__)
class Http(models.AbstractModel):
_inherit = 'ir.http'
@api.model
def _verify_request_recaptcha_token(self, action):
""" Verify the recaptcha token for the current request.
If no recaptcha private key is set the recaptcha verification
is considered inactive and this method will return True.
"""
ip_addr = request.httprequest.remote_addr
token = request.params.pop('recaptcha_token_response', False)
recaptcha_result = request.env['ir.http']._verify_recaptcha_token(ip_addr, token, 'website_form')
if recaptcha_result in ['is_human', 'no_secret']:
return True
if recaptcha_result == 'wrong_secret':
raise ValidationError(_("The reCaptcha private key is invalid."))
elif recaptcha_result == 'wrong_token':
raise ValidationError(_("The reCaptcha token is invalid."))
elif recaptcha_result == 'timeout':
raise UserError(_("Your request has timed out, please retry."))
elif recaptcha_result == 'bad_request':
raise UserError(_("The request is invalid or malformed."))
else:
return False
@api.model
def _verify_recaptcha_token(self, ip_addr, token, action=False):
"""
Verify a recaptchaV3 token and returns the result as a string.
RecaptchaV3 verify DOC: https://developers.google.com/recaptcha/docs/verify
:return: The result of the call to the google API:
is_human: The token is valid and the user trustworthy.
is_bot: The user is not trustworthy and most likely a bot.
no_secret: No reCaptcha secret set in settings.
wrong_action: the action performed to obtain the token does not match the one we are verifying.
wrong_token: The token provided is invalid or empty.
wrong_secret: The private key provided in settings is invalid.
timeout: The request has timout or the token provided is too old.
bad_request: The request is invalid or malformed.
:rtype: str
"""
private_key = request.env['ir.config_parameter'].sudo().get_param('recaptcha_private_key')
if not private_key:
return 'no_secret'
min_score = request.env['ir.config_parameter'].sudo().get_param('recaptcha_min_score')
try:
r = requests.post('https://www.recaptcha.net/recaptcha/api/siteverify', {
'secret': private_key,
'response': token,
'remoteip': ip_addr,
}, timeout=2) # it takes ~50ms to retrieve the response
result = r.json()
res_success = result['success']
res_action = res_success and action and result['action']
except requests.exceptions.Timeout:
logger.error("Trial captcha verification timeout for ip address %s", ip_addr)
return 'timeout'
except Exception:
logger.error("Trial captcha verification bad request response")
return 'bad_request'
if res_success:
score = result.get('score', False)
if score < float(min_score):
logger.warning("Trial captcha verification for ip address %s failed with score %f.", ip_addr, score)
return 'is_bot'
if res_action and res_action != action:
logger.warning("Trial captcha verification for ip address %s failed with action %f, expected: %s.", ip_addr, score, action)
return 'wrong_action'
logger.info("Trial captcha verification for ip address %s succeeded with score %f.", ip_addr, score)
return 'is_human'
errors = result.get('error-codes', [])
logger.warning("Trial captcha verification for ip address %s failed error codes %r. token was: [%s]", ip_addr, errors, token)
for error in errors:
if error in ['missing-input-secret', 'invalid-input-secret']:
return 'wrong_secret'
if error in ['missing-input-response', 'invalid-input-response']:
return 'wrong_token'
if error == 'timeout-or-duplicate':
return 'timeout'
if error == 'bad-request':
return 'bad_request'
return 'is_bot'
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import requests
from odoo import _, api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
recaptcha_public_key = fields.Char("Site Key", config_parameter='recaptcha_public_key', groups='base.group_system')
recaptcha_private_key = fields.Char("Secret Key", config_parameter='recaptcha_private_key', groups='base.group_system')
recaptcha_min_score = fields.Float("Minimum score", config_parameter='recaptcha_min_score', groups='base.group_system', default="0.5", help="Should be between 0.0 and 1.0")
@@ -0,0 +1,60 @@
odoo.define('google_recaptcha.ReCaptchaV3', function (require) {
"use strict";
const ajax = require('web.ajax');
const Class = require('web.Class');
const core = require('web.core');
const _t = core._t;
const ReCaptcha = Class.extend({
/**
* @override
*/
init: function () {
this._publicKey = odoo.reCaptchaPublicKey;
},
/**
* Loads the recaptcha libraries.
*
* @returns {Promise|boolean} promise if libs are loading else false if the reCaptcha key is empty.
*/
loadLibs: function () {
if (this._publicKey) {
this._recaptchaReady = ajax.loadJS(`https://www.recaptcha.net/recaptcha/api.js?render=${this._publicKey}`)
.then(() => new Promise(resolve => window.grecaptcha.ready(() => resolve())));
return this._recaptchaReady.then(() => !!document.querySelector('.grecaptcha-badge'));
}
return false;
},
/**
* Returns an object with the token if reCaptcha call succeeds
* If no key is set an object with a message is returned
* If an error occured an object with the error message is returned
*
* @param {string} action
* @returns {Promise|Object}
*/
getToken: async function (action) {
if (!this._publicKey) {
return {
message: _t("No recaptcha public key set."),
};
}
await this._recaptchaReady;
try {
return {
token: await window.grecaptcha.execute(this._publicKey, {action: action})
};
} catch (e) {
return {
error: _t("The recaptcha public key is invalid."),
};
}
},
});
return {
ReCaptcha: ReCaptcha,
};
});
@@ -0,0 +1,5 @@
// Hide google recaptcha V3 don't forget to add legal
// https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed
.grecaptcha-badge {
visibility: hidden;
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<t t-name="google_recaptcha.recaptcha_legal_terms">
<small class="o_recaptcha_legal_terms">
Protected by reCAPTCHA,
<a href="https://policies.google.com/privacy" target="_blank">Privacy Policy</a>
&amp;
<a href="https://policies.google.com/terms" target="_blank">Terms of Service</a>
apply.
</small>
</t>
</odoo>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/google_recaptcha/static/src/scss/recaptcha.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<t t-set="recaptcha_public_key" t-value="request.env['ir.config_parameter'].sudo().get_param('recaptcha_public_key')"/>
<script t-if="recaptcha_public_key" type="text/javascript">
odoo.reCaptchaPublicKey = '<t t-raw="recaptcha_public_key"/>';
</script>
<script type="text/javascript" src="/google_recaptcha/static/src/js/recaptcha.js"/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.web.recaptcha</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="recaptcha_warning" position="replace">
<div attrs="{'invisible': [('module_google_recaptcha', '=', False)]}">
<div class="content-group mt16">
<label for="recaptcha_public_key" class="o_light_label"/>
<field name="recaptcha_public_key"/>
</div>
<div class="content-group">
<label for="recaptcha_private_key" class="o_light_label"/>
<field name="recaptcha_private_key"/>
</div>
<div class="content-group">
<label for="recaptcha_min_score" class="o_light_label"/>
<field name="recaptcha_min_score"/>
</div>
<div>
<a href="https://www.google.com/recaptcha/admin/create" class="oe_link" target="_blank">
<i class="fa fa-arrow-right"/> Generate site/secret keys for v3 reCaptcha type
</a>
</div>
</div>
</div>
</field>
</record>
</odoo>
+2 -3
View File
@@ -18,8 +18,7 @@ class WebsiteForm(WebsiteForm):
return ['phone', 'mobile']
# Check and insert values from the form on the model <model> + validation phone fields
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
def website_form(self, model_name, **kwargs):
def _handle_website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].sudo().search([('model', '=', model_name), ('website_form_access', '=', True)])
if model_record and hasattr(request.env[model_name], 'phone_format'):
try:
@@ -46,7 +45,7 @@ class WebsiteForm(WebsiteForm):
state = request.env['res.country.state'].search([('code', '=', geoip_state_code), ('country_id.code', '=', geoip_country_code)])
if state:
request.params['state_id'] = state.id
return super(WebsiteForm, self).website_form(model_name, **kwargs)
return super(WebsiteForm, self)._handle_website_form(model_name, **kwargs)
def insert_record(self, request, model, values, custom, meta=None):
is_lead_model = model.model == 'crm.lead'
+1 -1
View File
@@ -7,7 +7,7 @@
This module adds a new building block in the website builder in order to build new forms from scratch in any website page.
""",
'version': '1.0',
'depends': ['website', 'mail'],
'depends': ['website', 'mail', 'google_recaptcha'],
'data': [
'data/mail_mail_data.xml',
'views/assets.xml',
+16 -3
View File
@@ -8,11 +8,11 @@ import pytz
from datetime import datetime
from psycopg2 import IntegrityError
from odoo import http, SUPERUSER_ID
from odoo import http, SUPERUSER_ID, _
from odoo.http import request
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.translate import _
from odoo.exceptions import ValidationError
from odoo.exceptions import ValidationError, UserError
from odoo.addons.base.models.ir_qweb_fields import nl2br
@@ -21,9 +21,22 @@ class WebsiteForm(http.Controller):
# Check and insert values from the form on the model <model>
@http.route('/website_form/<string:model_name>', type='http', auth="public", methods=['POST'], website=True)
def website_form(self, model_name, **kwargs):
try:
if request.env['ir.http']._verify_request_recaptcha_token('website_form'):
return self._handle_website_form(model_name, **kwargs)
error = _("Suspicious activity detected by Google reCaptcha.")
except (ValidationError, UserError) as e:
error = e.args[0]
return json.dumps({
'error': error,
})
def _handle_website_form(self, model_name, **kwargs):
model_record = request.env['ir.model'].sudo().search([('model', '=', model_name), ('website_form_access', '=', True)])
if not model_record:
return json.dumps(False)
return json.dumps({
'error': _("The form's specified model does not exist")
})
try:
data = self.extract_data(model_record, request.params)
@@ -3,6 +3,7 @@ odoo.define('website_form.s_website_form', function (require) {
var core = require('web.core');
var time = require('web.time');
const {ReCaptcha} = require('google_recaptcha.ReCaptchaV3');
var ajax = require('web.ajax');
var publicWidget = require('web.public.widget');
@@ -21,11 +22,19 @@ odoo.define('website_form.s_website_form', function (require) {
*/
init: function () {
this._super(...arguments);
this._recaptcha = new ReCaptcha();
this.__started = new Promise(resolve => this.__startResolve = resolve);
},
willStart: function () {
const res = this._super(...arguments);
if (!this.$target[0].classList.contains('s_website_form_no_recaptcha')) {
this._recaptcha.loadLibs();
}
return res;
},
start: function () {
var self = this;
// Initialize datetimepickers
var datepickers_options = {
minDate: moment({y: 1900}),
@@ -86,7 +95,7 @@ odoo.define('website_form.s_website_form', function (require) {
this.$target.parent().find('.s_website_form_end_message').addClass('d-none');
},
send: function (e) {
send: async function (e) {
e.preventDefault(); // Prevent the default submit behavior
// Prevent users from crazy clicking
this.$target.find('.s_website_form_send, .o_website_form_send').addClass('disabled'); // !compatibility
@@ -95,7 +104,7 @@ odoo.define('website_form.s_website_form', function (require) {
self.$target.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility
if (!self.check_error_fields({})) {
self.update_status('invalid');
self.update_status('error', _t("Please fill in the form correctly."));
return false;
}
@@ -132,6 +141,13 @@ odoo.define('website_form.s_website_form', function (require) {
}
});
const tokenObj = await this._recaptcha.getToken('website_form');
if (tokenObj.token) {
form_values['recaptcha_token_response'] = tokenObj.token;
} else if (tokenObj.error) {
self.update_status('error', tokenObj.error);
return false;
}
// Post form and handle result
ajax.post(this.$target.attr('action') + (this.$target.data('force_action') || this.$target.data('model_name')), form_values)
.then(function (result_data) {
@@ -140,7 +156,7 @@ odoo.define('website_form.s_website_form', function (require) {
result_data = JSON.parse(result_data);
if (!result_data.id) {
// Failure, the server didn't return the created record ID
self.update_status('error');
self.update_status('error', result_data.error ? result_data.error : false);
if (result_data.error_fields) {
// If the server return a list of bad fields, show these fields for users
self.check_error_fields(result_data.error_fields);
@@ -265,17 +281,23 @@ odoo.define('website_form.s_website_form', function (require) {
return value;
},
update_status: function (status) {
update_status: function (status, message) {
if (status !== 'success') { // Restore send button behavior if result is an error
this.$target.find('.s_website_form_send, .o_website_form_send').removeClass('disabled'); // !compatibility
}
var $result = this.$('#s_website_form_result, #o_website_form_result'); // !compatibility
if (status === 'error' && !message) {
message = _t("An error has occured, the form has not been sent.");
}
// Note: we still need to wait that the widget is properly started
// before any qweb rendering which depends on xmlDependencies
// because the event handlers are binded before the call to
// willStart for public widgets...
this.__started.then(() => $result.replaceWith(qweb.render(`website_form.status_${status}`)));
this.__started.then(() => $result.replaceWith(qweb.render(`website_form.status_${status}`, {
message: message,
})));
},
});
});
@@ -43,15 +43,15 @@
padding-bottom: 0.5rem;
}
.s_website_form_submit {
.s_website_form_submit, .s_website_form_recaptcha {
.s_website_form_label {
float: left;
height: 1px;
}
&.s_website_form_no_submit_label {
.s_website_form_label {
display: none;
}
}
.s_website_form_no_submit_label {
.s_website_form_label {
display: none;
}
}
}
@@ -9,7 +9,10 @@ const qweb = core.qweb;
const _t = core._t;
const FormEditor = options.Class.extend({
xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'],
xmlDependencies: [
'/website_form/static/src/xml/website_form_editor.xml',
'/google_recaptcha/static/src/xml/recaptcha.xml',
],
//----------------------------------------------------------------------
// Private
@@ -340,8 +343,8 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
const proms = [this._super(...arguments)];
// Disable text edition
this.$target.attr('contentEditable', false);
// Make button editable
this.$target.find('.s_website_form_send').attr('contentEditable', true);
// Make button and recaptcha editable
this.$target.find('.s_website_form_send, .s_website_form_recaptcha').attr('contentEditable', true);
// Get potential message
this.$message = this.$target.parent().find('.s_website_form_end_message');
this.showEndMessage = false;
@@ -479,6 +482,22 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
this.$target[0].dataset.mark = value.trim();
this._setLabelsMark();
},
/**
* Toggle the recaptcha legal terms
*/
toggleRecaptchaLegal: function (previewMode, value, params) {
const recaptchaLegalEl = this.$target[0].querySelector('.s_website_form_recaptcha');
if (recaptchaLegalEl) {
recaptchaLegalEl.remove();
} else {
const template = document.createElement('template');
const labelWidth = this.$target[0].querySelector('.s_website_form_label').style.width;
template.innerHTML = qweb.render("webite_form.s_website_form_recaptcha_legal", {labelWidth: labelWidth});
const legal = template.content.firstElementChild;
legal.setAttribute('contentEditable', true);
this.$target.find('.s_website_form_submit').before(legal);
}
},
//--------------------------------------------------------------------------
// Private
@@ -503,6 +522,8 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
return this.$target[0].dataset.successMode;
case 'setMark':
return this._getMark();
case 'toggleRecaptchaLegal':
return !this.$target[0].querySelector('.s_website_form_recaptcha') || '';
}
return this._super(...arguments);
},
@@ -643,7 +664,7 @@ options.registry.WebsiteFormEditor = FormEditor.extend({
await formInfo.formFields.forEach(async field => {
field.formatInfo = formatInfo;
await this._fetchFieldRecords(field);
this.$target.find('.s_website_form_submit').before(this._renderField(field));
this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(this._renderField(field));
});
}
},
@@ -1219,7 +1240,7 @@ options.registry.AddFieldForm = FormEditor.extend({
const field = this._getCustomField('char', 'Custom Text');
field.formatInfo = this._getDefaultFormat();
const htmlField = this._renderField(field);
this.$target.find('.s_website_form_submit').before(htmlField);
this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(htmlField);
this.trigger_up('activate_snippet', {
$snippet: $(htmlField),
});
@@ -8,17 +8,11 @@
</span>
</t>
<!-- Missing field status -->
<t t-name="website_form.status_invalid">
<span id="s_website_form_result" class="text-danger ml8">
<i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>Please fill in the form correctly.
</span>
</t>
<!-- Error status -->
<t t-name="website_form.status_error">
<span id="s_website_form_result" class="text-danger ml8">
<i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>An error has occured, the form has not been sent.
<i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>
<t t-esc="message"/>
</span>
</t>
</templates>
@@ -21,6 +21,15 @@
</div>
</t>
<t t-name="webite_form.s_website_form_recaptcha_legal">
<div class="col-12 s_website_form_recaptcha" data-name="Recaptcha Legal">
<div t-attf-style="width: #{labelWidth or '200px'}" class="s_website_form_label"/>
<div class="col-sm">
<t t-call="google_recaptcha.recaptcha_legal_terms"/>
</div>
</div>
</t>
<!-- Generic Field Layout -->
<!-- Changes made here needs to be reflected in the different Form view (Contact Us, Jobs, ...) -->
<t t-name="website_form.field">
@@ -39,6 +39,8 @@
<we-button class="fa fa-fw fa-eye align-self-end toggle-edit-message" title="Edit Message" data-name="message_opt" data-dependencies="show_message_opt"/>
</we-row>
<we-urlpicker string="URL" data-select-data-attribute="/contactus-thank-you" data-attribute-name="successPage" data-name="url_opt" data-dependencies="show_redirect_opt"/>
<t t-set="recaptcha_public_key" t-value="request.env['ir.config_parameter'].sudo().get_param('recaptcha_public_key')"/>
<we-checkbox t-if="recaptcha_public_key" string="Show reCaptcha Policy" data-toggle-recaptcha-legal="" data-no-preview="true"/>
</div>
<!-- Add Field Form -->
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _
from odoo.http import route, request
from odoo.osv import expression
from odoo.addons.mass_mailing.controllers.main import MassMailController
@@ -25,6 +26,11 @@ class MassMailController(MassMailController):
@route('/website_mass_mailing/subscribe', type='json', website=True, auth="public")
def subscribe(self, list_id, email, **post):
if not request.env['ir.http']._verify_request_recaptcha_token('website_mass_mailing_subscribe'):
return {
'toast_type': 'danger',
'toast_content': _("Suspicious activity detected by Google reCaptcha."),
}
ContactSubscription = request.env['mailing.contact.subscription'].sudo()
Contacts = request.env['mailing.contact'].sudo()
name, email = Contacts.get_name_email(email)
@@ -41,7 +47,10 @@ class MassMailController(MassMailController):
# add email to session
request.session['mass_mailing_email'] = email
mass_mailing_list = request.env['mailing.list'].sudo().browse(list_id)
return {'toast_content': mass_mailing_list.toast_content}
return {
'toast_type': 'success',
'toast_content': mass_mailing_list.toast_content
}
@route(['/website_mass_mailing/get_content'], type='json', website=True, auth="public")
def get_mass_mailing_content(self, newsletter_id, **post):
@@ -6,6 +6,8 @@ var rpc = require('web.rpc');
var WysiwygMultizone = require('web_editor.wysiwyg.multizone');
var options = require('web_editor.snippets.options');
var wUtils = require('website.utils');
const qweb = core.qweb;
var _t = core._t;
@@ -63,6 +65,39 @@ options.registry.mailing_list_subscribe = options.Class.extend({
},
});
options.registry.recaptchaSubscribe = options.Class.extend({
xmlDependencies: ['/google_recaptcha/static/src/xml/recaptcha.xml'],
/**
* Toggle the recaptcha legal terms
*/
toggleRecaptchaLegal: function (previewMode, value, params) {
const recaptchaLegalEl = this.$target[0].querySelector('.o_recaptcha_legal_terms');
if (recaptchaLegalEl) {
recaptchaLegalEl.remove();
} else {
const template = document.createElement('template');
template.innerHTML = qweb.render("google_recaptcha.recaptcha_legal_terms");
this.$target[0].appendChild(template.content.firstElementChild);
}
},
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @override
*/
_computeWidgetState: function (methodName, params) {
switch (methodName) {
case 'toggleRecaptchaLegal':
return !this.$target[0].querySelector('.o_recaptcha_legal_terms') || '';
}
return this._super(...arguments);
},
});
options.registry.newsletter_popup = options.registry.mailing_list_subscribe.extend({
popup_template_id: "editor_new_mailing_list_subscribe_popup",
popup_title: _t("Add a Newsletter Subscribe Popup"),
@@ -6,6 +6,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var utils = require('web.utils');
var publicWidget = require('web.public.widget');
const {ReCaptcha} = require('google_recaptcha.ReCaptchaV3');
var _t = core._t;
@@ -16,6 +17,20 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
'click .js_subscribe_btn': '_onSubscribeClick',
},
/**
* @constructor
*/
init: function () {
this._super(...arguments);
this._recaptcha = new ReCaptcha();
},
/**
* @override
*/
willStart: function () {
this._recaptcha.loadLibs();
return this._super(...arguments);
},
/**
* @override
*/
@@ -50,7 +65,9 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
* @override
*/
destroy: function () {
this.$target.addClass('d-none');
if (!this.editableMode) {
this.$target.addClass('d-none');
}
this._super.apply(this, arguments);
},
@@ -61,7 +78,7 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
/**
* @private
*/
_onSubscribeClick: function () {
_onSubscribeClick: async function () {
var self = this;
var $email = this.$(".js_subscribe_email:visible");
@@ -70,22 +87,35 @@ publicWidget.registry.subscribe = publicWidget.Widget.extend({
return false;
}
this.$target.removeClass('o_has_error').find('.form-control').removeClass('is-invalid');
const tokenObj = await this._recaptcha.getToken('website_form');
if (tokenObj.error) {
self.displayNotification({
type: 'danger',
title: _t("Error"),
message: tokenObj.error,
sticky: true,
});
return false;
}
this._rpc({
route: '/website_mass_mailing/subscribe',
params: {
'list_id': this.$target.data('list-id'),
'email': $email.length ? $email.val() : false,
recaptcha_token_response: tokenObj.token,
},
}).then(function (result) {
self.$(".js_subscribe_btn").addClass('d-none');
self.$(".js_subscribed_btn").removeClass('d-none');
self.$('input.js_subscribe_email').prop('disabled', !!result);
if (self.$popup.length) {
self.$popup.modal('hide');
if (result.type === 'success') {
self.$(".js_subscribe_btn").addClass('d-none');
self.$(".js_subscribed_btn").removeClass('d-none');
self.$('input.js_subscribe_email').prop('disabled', !!result);
if (self.$popup.length) {
self.$popup.modal('hide');
}
}
self.displayNotification({
type: 'success',
title: _t("Success"),
type: result.toast_type,
title: _t(`${result.toast_type === 'success' ? 'Success' : 'Error'}`),
message: result.toast_content,
sticky: true,
});
@@ -25,12 +25,14 @@
</template>
<template id="s_newsletter_subscribe_form" name="Newsletter">
<div class="input-group js_subscribe" data-list-id="0">
<input type="email" name="email" class="js_subscribe_email form-control" placeholder="your email..."/>
<span class="input-group-append">
<a role="button" href="#" class="btn btn-primary js_subscribe_btn">Subscribe</a>
<a role="button" href="#" class="btn btn-success js_subscribed_btn d-none" disabled="disabled">Thanks</a>
</span>
<div class="s_newsletter_subscribe_form js_subscribe" data-vxml="001" data-list-id="0">
<div class="input-group">
<input type="email" name="email" class="js_subscribe_email form-control" placeholder="your email..."/>
<span class="input-group-append">
<a role="button" href="#" class="btn btn-primary js_subscribe_btn">Subscribe</a>
<a role="button" href="#" class="btn btn-success js_subscribed_btn d-none" disabled="disabled">Thanks</a>
</span>
</div>
</div>
</template>
@@ -66,6 +68,11 @@
t-attf-data-exclude=".o_newsletter_modal #{selector}">
<we-button data-select_mailing_list="" data-no-preview="true">Change Newsletter</we-button>
</div>
<div data-js="recaptchaSubscribe"
t-att-data-selector="selector">
<t t-set="recaptcha_public_key" t-value="request.env['ir.config_parameter'].sudo().get_param('recaptcha_public_key')"/>
<we-checkbox t-if="recaptcha_public_key" string="Show reCaptcha Policy" data-toggle-recaptcha-legal="" data-no-preview="true"/>
</div>
<div t-att-data-selector="selector" data-drop-near="p, h1, h2, h3, blockquote, .card"/>
<div data-js="newsletter_popup"
data-selector=".o_newsletter_popup">
+1 -1
View File
@@ -748,7 +748,7 @@
<div class="col-12 col-xl order-xl-1 oe_cart">
<section class="s_website_form" data-vcss="001" data-snippet="s_website_form">
<div class="container">
<form action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-force_action="shop.sale.order" data-model_name="sale.order" data-success-mode="redirect" data-success-page="/shop/payment" hide-change-model="true">
<form action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required s_website_form_no_recaptcha" data-mark="*" data-force_action="shop.sale.order" data-model_name="sale.order" data-success-mode="redirect" data-success-page="/shop/payment" hide-change-model="true">
<div class="s_website_form_rows row s_col_no_bgcolor">
<div class="form-group col-12 s_website_form_field" data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">