diff options
| author | Martin Trigaux <mat@odoo.com> | 2016-04-20 13:29:25 +0200 |
|---|---|---|
| committer | Martin Trigaux <mat@odoo.com> | 2016-07-05 17:24:36 +0200 |
| commit | 2407f793a68d4ac348ba13f0dc0b8b974660a0fd (patch) | |
| tree | a0af17074987c19f5985e1c3550b6945c290b880 /addons | |
| parent | 26be95592c260e451acd42e86408326a1a39844c (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.
Diffstat (limited to 'addons')
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> |
