From b1f88d634d603b74a62d051032e4003fc6f7a3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 27 Feb 2014 14:48:29 +0100 Subject: [PATCH 1/3] [IMP] [FIX] email_template: do not sanitize the template content. Indeed its content may contain invalid html that could be stripped by the sanitizer. The content generated based on the template will be sanitized when stored in the mail_mail or mail_message body field, thus after rendering. The template therefore holds html, but that is not sanitized. But that's still html, therefore using an html field. bzr revid: tde@openerp.com-20140227134829-te8mxeakc3s96fun --- addons/email_template/email_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index fcfe869da90..0b5945c1f0b 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -183,7 +183,7 @@ class email_template(osv.osv): 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False, help="Optional preferred server for outgoing mails. If not set, the highest " "priority one will be used."), - 'body_html': fields.html('Body', translate=True, help="Rich-text/HTML version of the message (placeholders may be used here)"), + 'body_html': fields.html('Body', translate=True, sanitize=False, help="Rich-text/HTML version of the message (placeholders may be used here)"), 'report_name': fields.char('Report Filename', translate=True, help="Name to use for the generated report file (may contain placeholders)\n" "The extension can be omitted and will then come from the report type."), From a9a4767d17650320cba710ce99b6c737419df7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 27 Feb 2014 16:38:35 +0100 Subject: [PATCH 2/3] [FIX] email_template : - fixed composer using template that were rendering the body twice, once form the template and once from the composer body. Only the latter one is used, so avoid generating the template body that is not necessary - fixed email_template generating values for a set of given fields, ignoring the field list given into parameter - fixed post processing of templates to transform local urls into absolute urls; now urls are transformed after body generation, when sending email based on templates , or when generating the content when using the composer. bzr revid: tde@openerp.com-20140227153835-gmqnxrzed9fnbxhm --- addons/email_template/email_template.py | 6 ++-- .../wizard/mail_compose_message.py | 15 +++++--- addons/website_mail/models/__init__.py | 1 + addons/website_mail/models/email_template.py | 16 ++++----- .../models/mail_compose_message.py | 34 +++++++++++++++++++ 5 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 addons/website_mail/models/mail_compose_message.py diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 0b5945c1f0b..d9b65849fae 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -356,17 +356,17 @@ class email_template(osv.osv): results = dict() for template, template_res_ids in templates_to_res_ids.iteritems(): # generate fields value for all res_ids linked to the current template - for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']: + for field in fields: generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value # update values for all res_ids for res_id in template_res_ids: values = results[res_id] - if template.user_signature: + if 'body_html' in fields and template.user_signature: signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature values['body_html'] = tools.append_content_to_html(values['body_html'], signature) - if values['body_html']: + if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) values.update( mail_server_id=template.mail_server_id.id or False, diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index beede1122dc..3f926439d08 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -162,16 +162,18 @@ class mail_compose_message(osv.TransientModel): partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) return partner_ids - def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None): + def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ # filter template values - fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id'] + if fields is None: + fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] + returned_fields = fields + ['attachments'] values = dict.fromkeys(res_ids, False) - template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context) + template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context) for res_id in res_ids: - res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field)) + res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field)) res_id_values['body'] = res_id_values.pop('body_html', '') # transform email_to, email_cc into partner_ids @@ -189,7 +191,10 @@ class mail_compose_message(osv.TransientModel): """ Override to handle templates. """ # generate template-based values if wizard.template_id: - template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context) + template_values = self.generate_email_for_composer_batch( + cr, uid, wizard.template_id.id, res_ids, + fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'], + context=context) else: template_values = dict.fromkeys(res_ids, dict()) # generate composer values diff --git a/addons/website_mail/models/__init__.py b/addons/website_mail/models/__init__.py index e60de014149..3e374ccddd2 100644 --- a/addons/website_mail/models/__init__.py +++ b/addons/website_mail/models/__init__.py @@ -1,3 +1,4 @@ import mail_message import mail_thread import email_template +import mail_compose_message \ No newline at end of file diff --git a/addons/website_mail/models/email_template.py b/addons/website_mail/models/email_template.py index aed01c179f7..105fcff4428 100644 --- a/addons/website_mail/models/email_template.py +++ b/addons/website_mail/models/email_template.py @@ -79,12 +79,10 @@ class EmailTemplate(osv.Model): html = html[5:-6] return html - def create(self, cr, uid, values, context=None): - if 'body_html' in values: - values['body_html'] = self._postprocess_html_replace_links(cr, uid, values['body_html'], context=context) - return super(EmailTemplate, self).create(cr, uid, values, context=context) - - def write(self, cr, uid, ids, values, context=None): - if 'body_html' in values: - values['body_html'] = self._postprocess_html_replace_links(cr, uid, values['body_html'], context=context) - return super(EmailTemplate, self).write(cr, uid, ids, values, context=context) + def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): + """ Add a post processing after rendering, aka replace local URLs to absolute URLs. """ + results = super(EmailTemplate, self).generate_email_batch(cr, uid, template_id, res_ids, context=context, fields=fields) + for res_id, value in results.iteritems(): + if 'body_html' in value: + results[res_id]['body_html'] = self._postprocess_html_replace_links(cr, uid, value['body_html'], context=context) + return results diff --git a/addons/website_mail/models/mail_compose_message.py b/addons/website_mail/models/mail_compose_message.py new file mode 100644 index 00000000000..bc8258bbb6f --- /dev/null +++ b/addons/website_mail/models/mail_compose_message.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import osv + + +class MailComposeMessage(osv.Model): + _inherit = 'mail.compose.message' + + def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): + """ Add a post processing after rendering, aka replace local URLs to absolute URLs. """ + results = super(MailComposeMessage, self).generate_email_for_composer_batch(cr, uid, template_id, res_ids, context=context, fields=fields) + for res_id, value in results.iteritems(): + if 'body' in value: + results[res_id]['body'] = self.pool['email.template']._postprocess_html_replace_links(cr, uid, value['body'], context=context) + return results From 05fdc2290a2c8ef31f8a0df393cba29b196f6e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 3 Mar 2014 11:40:44 +0100 Subject: [PATCH 3/3] [IMP] email_template: relocate url rewritign stuff directly into the rendering method. bzr revid: tde@openerp.com-20140303104044-hgmii31ga6msf72z --- addons/email_template/email_template.py | 55 ++++++++++++++++++- addons/website_mail/models/__init__.py | 3 +- addons/website_mail/models/email_template.py | 50 ----------------- .../models/mail_compose_message.py | 34 ------------ 4 files changed, 54 insertions(+), 88 deletions(-) delete mode 100644 addons/website_mail/models/mail_compose_message.py diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index d9b65849fae..4a8e6f51706 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -24,6 +24,8 @@ import base64 import datetime import dateutil.relativedelta as relativedelta import logging +import lxml +import urlparse import openerp from openerp import SUPERUSER_ID @@ -70,6 +72,7 @@ try: except ImportError: _logger.warning("jinja2 not available, templating features will not work!") + class email_template(osv.osv): "Templates for sending email" _name = "email.template" @@ -82,7 +85,48 @@ class email_template(osv.osv): res['model_id'] = self.pool['ir.model'].search(cr, uid, [('model', '=', res.pop('model'))], context=context)[0] return res - def render_template_batch(self, cr, uid, template, model, res_ids, context=None): + def _replace_local_links(self, cr, uid, html, context=None): + """ Post-processing of html content to replace local links to absolute + links, using web.base.url as base url. """ + if not html: + return html + + # form a tree + root = lxml.html.fromstring(html) + if not len(root) and root.text is None and root.tail is None: + html = '
%s
' % html + root = lxml.html.fromstring(html) + + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url) + + def _process_link(url): + new_url = url + (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) + if not scheme and not netloc: + new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment)) + return new_url + + # check all nodes, replace : + # - img src -> check URL + # - a href -> check URL + for node in root.iter(): + if node.tag == 'a': + node.set('href', _process_link(node.get('href'))) + elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): + node.set('src', _process_link(node.get('src'))) + + html = lxml.html.tostring(root, pretty_print=False, method='html') + # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that + if html.startswith('
') and html.endswith('
'): + html = html[5:-6] + return html + + def render_post_process(self, cr, uid, html, context=None): + html = self._replace_local_links(cr, uid, html, context=context) + return html + + def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_processing=None): """Render the given template text, replace mako expressions ``${expr}`` with the result of evaluating these expressions with an evaluation context containing: @@ -125,6 +169,10 @@ class email_template(osv.osv): if render_result == u"False": render_result = u"" results[res_id] = render_result + + if post_processing: + for res_id, result in results.iteritems(): + results[res_id] = self.render_post_process(cr, uid, result, context=context) return results def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None): @@ -357,7 +405,10 @@ class email_template(osv.osv): for template, template_res_ids in templates_to_res_ids.iteritems(): # generate fields value for all res_ids linked to the current template for field in fields: - generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context) + generated_field_values = self.render_template_batch( + cr, uid, getattr(template, field), template.model, template_res_ids, + post_processing=(field == 'body_html'), + context=context) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value # update values for all res_ids diff --git a/addons/website_mail/models/__init__.py b/addons/website_mail/models/__init__.py index 3e374ccddd2..fc2ecbbc9bb 100644 --- a/addons/website_mail/models/__init__.py +++ b/addons/website_mail/models/__init__.py @@ -1,4 +1,3 @@ import mail_message import mail_thread -import email_template -import mail_compose_message \ No newline at end of file +import email_template \ No newline at end of file diff --git a/addons/website_mail/models/email_template.py b/addons/website_mail/models/email_template.py index 105fcff4428..beb02739f29 100644 --- a/addons/website_mail/models/email_template.py +++ b/addons/website_mail/models/email_template.py @@ -19,9 +19,6 @@ # ############################################################################## -import lxml -import urlparse - from openerp.osv import osv, fields from openerp.tools.translate import _ @@ -39,50 +36,3 @@ class EmailTemplate(osv.Model): help='Link to the website', ), } - - def _postprocess_html_replace_links(self, cr, uid, body_html, context=None): - """ Post-processing of body_html. Indeed the content generated by the - website builder contains references to local addresses, for example - for images. This method changes those addresses to absolute addresses. """ - html = body_html - if not body_html: - return html - - # form a tree - root = lxml.html.fromstring(html) - if not len(root) and root.text is None and root.tail is None: - html = '
%s
' % html - root = lxml.html.fromstring(html) - - base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') - (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url) - - def _process_link(url): - new_url = url - (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) - if not scheme and not netloc: - new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment)) - return new_url - - # check all nodes, replace : - # - img src -> check URL - # - a href -> check URL - for node in root.iter(): - if node.tag == 'a': - node.set('href', _process_link(node.get('href'))) - elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): - node.set('src', _process_link(node.get('src'))) - - html = lxml.html.tostring(root, pretty_print=False, method='html') - # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that - if html.startswith('
') and html.endswith('
'): - html = html[5:-6] - return html - - def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): - """ Add a post processing after rendering, aka replace local URLs to absolute URLs. """ - results = super(EmailTemplate, self).generate_email_batch(cr, uid, template_id, res_ids, context=context, fields=fields) - for res_id, value in results.iteritems(): - if 'body_html' in value: - results[res_id]['body_html'] = self._postprocess_html_replace_links(cr, uid, value['body_html'], context=context) - return results diff --git a/addons/website_mail/models/mail_compose_message.py b/addons/website_mail/models/mail_compose_message.py deleted file mode 100644 index bc8258bbb6f..00000000000 --- a/addons/website_mail/models/mail_compose_message.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2014-Today OpenERP SA (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp.osv import osv - - -class MailComposeMessage(osv.Model): - _inherit = 'mail.compose.message' - - def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): - """ Add a post processing after rendering, aka replace local URLs to absolute URLs. """ - results = super(MailComposeMessage, self).generate_email_for_composer_batch(cr, uid, template_id, res_ids, context=context, fields=fields) - for res_id, value in results.iteritems(): - if 'body' in value: - results[res_id]['body'] = self.pool['email.template']._postprocess_html_replace_links(cr, uid, value['body'], context=context) - return results