[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:
Martin Trigaux
2016-07-05 17:24:36 +02:00
parent 26be95592c
commit 2407f793a6
7 changed files with 187 additions and 23 deletions
@@ -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',
],
+51 -12
View File
@@ -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>