[FIX] website_mail_channel: public user (un)registration
Allows public users to unsubscribe to a mailing list. Before this patch, a user had to be connected with an account with subscribing. For public users, a confirmation email is sent to subscribe and unsubscribe to a list. Use hmac signatures to prevent fake (un)subscriptions.
This commit is contained in:
@@ -10,6 +10,7 @@ Odoo Mail Group : Mailing List Archives
|
||||
""",
|
||||
'depends': ['website_mail'],
|
||||
'data': [
|
||||
'data/mail_template_data.xml',
|
||||
'views/website_mail_channel_templates.xml',
|
||||
'views/snippets.xml',
|
||||
],
|
||||
|
||||
@@ -89,23 +89,30 @@ class MailGroup(http.Controller):
|
||||
|
||||
# search partner_id
|
||||
if request.env.user != request.website.user_id:
|
||||
# connected users are directly (un)subscribed
|
||||
partner_ids = request.env.user.partner_id.ids
|
||||
else: # mail_thread method
|
||||
|
||||
# add or remove channel members
|
||||
if unsubscribe:
|
||||
channel.check_access_rule('read')
|
||||
channel.sudo().write({'channel_partner_ids': [(3, partner_id) for partner_id in partner_ids]})
|
||||
return "off"
|
||||
else: # add partner to the channel
|
||||
request.session['partner_id'] = partner_ids[0]
|
||||
channel.check_access_rule('read')
|
||||
channel.sudo().write({'channel_partner_ids': [(4, partner_id) for partner_id in partner_ids]})
|
||||
return "on"
|
||||
|
||||
else:
|
||||
# public users will recieve confirmation email
|
||||
partner_ids = channel.sudo()._find_partner_from_emails([email], check_followers=True)
|
||||
if not partner_ids or not partner_ids[0]:
|
||||
name = email.split('@')[0]
|
||||
partner_ids = request.env['res.partner'].sudo().create({'name': name, 'email': email}).ids
|
||||
partner_ids = [request.env['res.partner'].sudo().create({'name': name, 'email': email}).id]
|
||||
|
||||
channel.sudo()._send_confirmation_email(partner_ids, unsubscribe)
|
||||
return "email"
|
||||
|
||||
# add or remove channel members
|
||||
if unsubscribe:
|
||||
channel.check_access_rule('read')
|
||||
channel.sudo().write({'channel_partner_ids': [(3, partner_id) for partner_id in partner_ids]})
|
||||
return False
|
||||
else: # add partner to the channel
|
||||
request.session['partner_id'] = partner_ids[0]
|
||||
channel.check_access_rule('read')
|
||||
channel.sudo().write({'channel_partner_ids': [(4, partner_id) for partner_id in partner_ids]})
|
||||
return True
|
||||
|
||||
@http.route([
|
||||
"/groups/<model('mail.channel'):group>",
|
||||
@@ -189,3 +196,35 @@ class MailGroup(http.Controller):
|
||||
return {
|
||||
'alias_name': group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain and '%s@%s' % (group.alias_id.alias_name, group.alias_id.alias_domain) or False
|
||||
}
|
||||
|
||||
@http.route("/groups/subscribe/<model('mail.channel'):channel>/<int:partner_id>/<string:token>", type='http', auth='public', website=True)
|
||||
def confirm_subscribe(self, channel, partner_id, token, **kw):
|
||||
subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)])
|
||||
if subscriber:
|
||||
# already registered, maybe clicked twice
|
||||
return request.render('website_mail_channel.invalid_token_subscription')
|
||||
|
||||
subscriber_token = channel._generate_action_token(partner_id, action='subscribe')
|
||||
if token != subscriber_token:
|
||||
return request.render('website_mail_channel.invalid_token_subscription')
|
||||
|
||||
# add partner
|
||||
channel.sudo().write({'channel_partner_ids': [(4, partner_id)]})
|
||||
|
||||
return request.website.render("website_mail_channel.confirmation_subscription", {'subscribing': True})
|
||||
|
||||
@http.route("/groups/unsubscribe/<model('mail.channel'):channel>/<int:partner_id>/<string:token>", type='http', auth='public', website=True)
|
||||
def confirm_unsubscribe(self, channel, partner_id, token, **kw):
|
||||
subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)])
|
||||
if not subscriber:
|
||||
# not registered, maybe already unsubsribed
|
||||
return request.render('website_mail_channel.invalid_token_subscription')
|
||||
|
||||
subscriber_token = channel._generate_action_token(partner_id, action='unsubscribe')
|
||||
if token != subscriber_token:
|
||||
return request.render('website_mail_channel.invalid_token_subscription')
|
||||
|
||||
# remove partner
|
||||
channel.sudo().write({'channel_partner_ids': [(3, partner_id)]})
|
||||
|
||||
return request.website.render("website_mail_channel.confirmation_subscription", {'subscribing': False})
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="mail_template_list_subscribe" model="mail.template">
|
||||
<field name="name">Subscribe to mailing list</field>
|
||||
<field name="model_id" ref="mail.model_mail_channel"/>
|
||||
<field name="subject">Please confirm subscription to ${object.name}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<p>Hello,</p>
|
||||
<p>You have requested to be subscribed to the mailing list <strong>${object.name}</strong></p>
|
||||
<p>To confirm, please visit the following link:</p>
|
||||
<p><strong><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong></p>
|
||||
<p>If this was a mistake or you did not requested this action, please ignore this message.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_template_list_unsubscribe" model="mail.template">
|
||||
<field name="name">Unsubscribe to mailing list</field>
|
||||
<field name="model_id" ref="mail.model_mail_channel"/>
|
||||
<field name="subject">Please confirm unsubscription to ${object.name}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<p>Hello,</p>
|
||||
<p>You have requested to be unsubscribed to the mailing list <strong>${object.name}</strong></p>
|
||||
<p>To confirm, please visit the following link:</p>
|
||||
<p><strong><a href="${ctx['token_url']}">${ctx['token_url']}</a></strong></p>
|
||||
<p>If this was a mistake or you did not requested this action, please ignore this message.</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,6 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hmac
|
||||
|
||||
from urlparse import urljoin
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools.safe_eval import safe_eval as eval
|
||||
from odoo.addons.website.models.website import slug
|
||||
@@ -27,3 +31,41 @@ class MailGroup(models.Model):
|
||||
})
|
||||
res['headers'] = repr(headers)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _send_confirmation_email(self, partner_ids, unsubscribe=False):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
route = "/groups/%(action)s/%(channel)s/%(partner)s/%(token)s"
|
||||
if unsubscribe:
|
||||
template = self.env.ref('website_mail_channel.mail_template_list_unsubscribe')
|
||||
action = 'unsubscribe'
|
||||
else:
|
||||
template = self.env.ref('website_mail_channel.mail_template_list_subscribe')
|
||||
action = 'subscribe'
|
||||
|
||||
for partner_id in partner_ids:
|
||||
# generate a new token per subscriber
|
||||
token = self._generate_action_token(partner_id, action=action)
|
||||
|
||||
token_url = urljoin(base_url, route % {
|
||||
'action': action,
|
||||
'channel': self.id,
|
||||
'partner': partner_id,
|
||||
'token': token,
|
||||
})
|
||||
template.with_context(token_url=token_url).send_mail(self.id,
|
||||
force_send=True,
|
||||
email_values={'recipient_ids': [(4, partner_id)]}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _generate_action_token(self, partner_id, action='unsubscribe'):
|
||||
self.ensure_one()
|
||||
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
|
||||
data = '$'.join([
|
||||
str(self.id),
|
||||
str(partner_id),
|
||||
action])
|
||||
return hmac.new(secret.encode('utf-8'), data).hexdigest()
|
||||
|
||||
@@ -18,7 +18,7 @@ animation.registry.follow_alias = animation.Class.extend({
|
||||
self.is_user = data.is_user;
|
||||
self.email = data.email;
|
||||
self.$target.find('.js_mg_link').attr('href', '/groups/' + self.$target.data('id'));
|
||||
self.toggle_subscription(data.is_member, data.email);
|
||||
self.toggle_subscription(data.is_member ? 'on' : 'off', data.email);
|
||||
self.$target.removeClass("hidden");
|
||||
});
|
||||
|
||||
@@ -43,29 +43,47 @@ animation.registry.follow_alias = animation.Class.extend({
|
||||
}
|
||||
this.$target.removeClass('has-error');
|
||||
|
||||
var subscription_action = this.$target.attr("data-follow") || "off";
|
||||
if (location.search.slice(1).split('&').indexOf("unsubscribe") >= 0) {
|
||||
// force unsubscribe mode via URI /groups?unsubscribe
|
||||
subscription_action = 'on';
|
||||
}
|
||||
ajax.jsonRpc('/groups/subscription', 'call', {
|
||||
'channel_id': +this.$target.data('id'),
|
||||
'object': this.$target.data('object'),
|
||||
'subscription': this.$target.attr("data-follow") || "off",
|
||||
'object': this.$target.data('object'),
|
||||
'subscription': subscription_action,
|
||||
'email': $email.length ? $email.val() : false,
|
||||
}).then(function (follow) {
|
||||
self.toggle_subscription(follow, self.email);
|
||||
});
|
||||
},
|
||||
toggle_subscription: function(follow, email) {
|
||||
// .js_mg_follow_form contains subscribe form
|
||||
// .js_mg_details contains send/archives/unsubscribe links
|
||||
// .js_mg_confirmation contains message warning has been sent
|
||||
var alias_done = this.get_alias_info();
|
||||
if (follow) {
|
||||
if (follow === "on") {
|
||||
// user is connected and can unsubscribe
|
||||
this.$target.find(".js_mg_follow_form").addClass("hidden");
|
||||
this.$target.find(".js_mg_details").removeClass("hidden");
|
||||
}
|
||||
else {
|
||||
this.$target.find(".js_mg_confirmation").addClass("hidden");
|
||||
} else if (follow === "off") {
|
||||
// user is connected and can subscribe
|
||||
this.$target.find(".js_mg_follow_form").removeClass("hidden");
|
||||
this.$target.find(".js_mg_details").addClass("hidden");
|
||||
this.$target.find(".js_mg_confirmation").addClass("hidden");
|
||||
} else if (follow === "email") {
|
||||
// a confirmation email has been sent
|
||||
this.$target.find(".js_mg_follow_form").addClass("hidden");
|
||||
this.$target.find(".js_mg_details").addClass("hidden");
|
||||
this.$target.find(".js_mg_confirmation").removeClass("hidden");
|
||||
} else {
|
||||
console.error("Unknown subscription action", follow)
|
||||
}
|
||||
this.$target.find('input.js_follow_email')
|
||||
.val(email ? email : "")
|
||||
.attr("disabled", follow || (email.length && this.is_user) ? "disabled" : false);
|
||||
this.$target.attr("data-follow", follow ? 'on' : 'off');
|
||||
.attr("disabled", follow === "on" || (email.length && this.is_user) ? "disabled" : false);
|
||||
this.$target.attr("data-follow", follow);
|
||||
return $.when(alias_done);
|
||||
},
|
||||
get_alias_info: function() {
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<i class="fa fa-file-o"/><a href="#" class="js_mg_link"> archives</a> -
|
||||
<i class="fa fa-times"/><a href="#" class="js_unfollow_btn"> unsubscribe</a>
|
||||
</p>
|
||||
<p class="js_mg_confirmation hidden well well-sm">
|
||||
a confirmation email has been sent.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -58,15 +58,18 @@
|
||||
<div class="js_follow_alias"
|
||||
t-att-data-id="group.id"
|
||||
data-object="mail.channel"
|
||||
data-follow="off">
|
||||
t-att-data-follow="'on' if 'unsubscribe' in request.params else 'off'">
|
||||
<div class="input-group js_mg_follow_form">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="js_follow_email form-control"
|
||||
placeholder="your email..."/>
|
||||
<span class="input-group-btn">
|
||||
<button href="#" class="btn btn-primary js_follow_btn">Subscribe</button>
|
||||
<span t-if="'unsubscribe' not in request.params" class="input-group-btn">
|
||||
<button href="#" class="btn btn-primary js_follow_btn">Subscribe</button>
|
||||
</span>
|
||||
<span t-if="'unsubscribe' in request.params" class="input-group-btn">
|
||||
<button href="#" class="btn btn-primary js_follow_btn">Unsubscribe</button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="js_mg_details hidden well well-sm">
|
||||
@@ -306,4 +309,32 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template id="confirmation_subscription" name="Mailing List Confirmation">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="container">
|
||||
<p>
|
||||
You have been correctly
|
||||
<t t-if="subscribing">subscribed</t>
|
||||
<t t-if="not subscribing">unsubscribed</t>
|
||||
to the mailing list.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="invalid_token_subscription" name="Invalid Token Submitted">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="container">
|
||||
<p>
|
||||
Invalid or expired confirmation link.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user