summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2016-04-20 13:29:25 +0200
committerMartin Trigaux <mat@odoo.com>2016-07-05 17:24:36 +0200
commit2407f793a68d4ac348ba13f0dc0b8b974660a0fd (patch)
treea0af17074987c19f5985e1c3550b6945c290b880
parent26be95592c260e451acd42e86408326a1a39844c (diff)
[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.
-rw-r--r--addons/website_mail_channel/__openerp__.py1
-rw-r--r--addons/website_mail_channel/controllers/main.py63
-rw-r--r--addons/website_mail_channel/data/mail_template_data.xml29
-rw-r--r--addons/website_mail_channel/models/mail_channel.py42
-rw-r--r--addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js34
-rw-r--r--addons/website_mail_channel/views/snippets.xml4
-rw-r--r--addons/website_mail_channel/views/website_mail_channel_templates.xml37
7 files changed, 187 insertions, 23 deletions
diff --git a/addons/website_mail_channel/__openerp__.py b/addons/website_mail_channel/__openerp__.py
index f38e434dcd8..ab541dac6b0 100644
--- a/addons/website_mail_channel/__openerp__.py
+++ b/addons/website_mail_channel/__openerp__.py
@@ -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',
],
diff --git a/addons/website_mail_channel/controllers/main.py b/addons/website_mail_channel/controllers/main.py
index 8cac7b0ee30..1b043caf8ac 100644
--- a/addons/website_mail_channel/controllers/main.py
+++ b/addons/website_mail_channel/controllers/main.py
@@ -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})
diff --git a/addons/website_mail_channel/data/mail_template_data.xml b/addons/website_mail_channel/data/mail_template_data.xml
new file mode 100644
index 00000000000..31c602f99a3
--- /dev/null
+++ b/addons/website_mail_channel/data/mail_template_data.xml
@@ -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>
diff --git a/addons/website_mail_channel/models/mail_channel.py b/addons/website_mail_channel/models/mail_channel.py
index d57d82c51a8..fb1db7ce97b 100644
--- a/addons/website_mail_channel/models/mail_channel.py
+++ b/addons/website_mail_channel/models/mail_channel.py
@@ -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()
diff --git a/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js b/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js
index 8a872783e5e..7fdc14f8c2a 100644
--- a/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js
+++ b/addons/website_mail_channel/static/src/js/website_mail_channel.snippet.js
@@ -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() {
diff --git a/addons/website_mail_channel/views/snippets.xml b/addons/website_mail_channel/views/snippets.xml
index 7439414b9d7..1cada8717b2 100644
--- a/addons/website_mail_channel/views/snippets.xml
+++ b/addons/website_mail_channel/views/snippets.xml
@@ -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>
diff --git a/addons/website_mail_channel/views/website_mail_channel_templates.xml b/addons/website_mail_channel/views/website_mail_channel_templates.xml
index 2c3060398c9..dfbebe1f4fe 100644
--- a/addons/website_mail_channel/views/website_mail_channel_templates.xml
+++ b/addons/website_mail_channel/views/website_mail_channel_templates.xml
@@ -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>