summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThibault Delavallée <tde@openerp.com>2016-08-19 13:49:48 +0200
committerThibault Delavallée <tde@openerp.com>2016-08-30 15:54:56 +0200
commit02c748be59ac8abc44c945218587e7b8a8e06a28 (patch)
tree1e68f807714a08b850c66b8d9c9b02e47fdc3a5b
parent97425bfe3f2b317595e7086aedf7b5689dc9d218 (diff)
[IMP] tools: sanitizer: add and clean sanitize options
Now having * sanitize: run the sanitizer to clean the html (removing javascripts, unwanted tags, ...) * sanitize_tags: only a subset of tags is allowed in html content. Unwelcomed tags are remove dand their content stripped. * sanitize_attributes: only a subset of attributes is allowed. * sanitize_style: only a subset of style attributes is allowed. Style attributes are parsed to keep only a white list. * strip_style: all style is removed. It bypasses sanitize_style as there is no need to sanitize something that is removed. * strip_classes: remove class attributes Fields parameters have also been updated to match the sanitize options. Html fields by default are sanitized with sanitize_tags activated but without any further options. All addons have been updated to match the new options according to their previous behavior.
-rw-r--r--addons/event/models/event.py2
-rw-r--r--addons/mail/models/mail_message.py2
-rw-r--r--addons/mail/models/mail_template.py2
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/website_blog/models/website_blog.py2
-rw-r--r--addons/website_event_track/models/website_event_track.py2
-rw-r--r--addons/website_forum/models/forum.py2
-rw-r--r--addons/website_hr_recruitment/models/hr_recruitment.py2
-rw-r--r--addons/website_livechat/models/im_livechat.py2
-rw-r--r--addons/website_mass_mailing/models/mass_mailing_list.py2
-rw-r--r--addons/website_quote/models/product_template.py4
-rw-r--r--addons/website_quote/models/sale_order.py4
-rw-r--r--addons/website_quote/models/sale_quote.py4
-rw-r--r--addons/website_sale/models/product.py2
-rw-r--r--addons/website_slides/models/slides.py4
-rw-r--r--openerp/addons/base/tests/test_mail.py13
-rw-r--r--openerp/addons/test_new_api/models.py6
-rw-r--r--openerp/addons/test_new_api/tests/test_new_fields.py4
-rw-r--r--openerp/fields.py25
-rw-r--r--openerp/osv/fields.py19
-rw-r--r--openerp/tools/mail.py63
21 files changed, 111 insertions, 57 deletions
diff --git a/addons/event/models/event.py b/addons/event/models/event.py
index d569a08984b..a0a7e72ae71 100644
--- a/addons/event/models/event.py
+++ b/addons/event/models/event.py
@@ -184,7 +184,7 @@ class EventEvent(models.Model):
readonly=False, states={'done': [('readonly', True)]})
country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True)
description = fields.Html(
- string='Description', oldname='note', translate=html_translate, sanitize=False,
+ string='Description', oldname='note', translate=html_translate, sanitize_attributes=False,
readonly=False, states={'done': [('readonly', True)]})
# badge fields
badge_front = fields.Html(string='Badge Front')
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 59f707bfc9f..5aa52d755c8 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -50,7 +50,7 @@ class Message(models.Model):
# content
subject = fields.Char('Subject')
date = fields.Datetime('Date', default=fields.Datetime.now)
- body = fields.Html('Contents', default='', strip_classes=True)
+ body = fields.Html('Contents', default='', sanitize_style=True, strip_classes=True)
attachment_ids = fields.Many2many(
'ir.attachment', 'message_attachment_rel',
'message_id', 'attachment_id',
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 8508fa9f250..801cf27f50e 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -139,7 +139,7 @@ class MailTemplate(models.Model):
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, sanitize=False)
+ body_html = fields.Html('Body', translate=True, sanitize_attributes=False)
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.")
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 9bf1a913b20..de197a48b89 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -300,7 +300,7 @@ class MassMailing(models.Model):
create_date = fields.Datetime(string='Creation Date')
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
schedule_date = fields.Datetime(string='Schedule in the Future')
- body_html = fields.Html(string='Body', translate=html_translate, sanitize=False)
+ body_html = fields.Html(string='Body', translate=html_translate, sanitize_attributes=False)
attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
'mass_mailing_id', 'attachment_id', string='Attachments')
keep_archives = fields.Boolean(string='Keep Archives')
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index d1a4ad93c56..35d611f162e 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -110,7 +110,7 @@ class BlogPost(models.Model):
default='{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}')
blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade')
tag_ids = fields.Many2many('blog.tag', string='Tags')
- content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False)
+ content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize_attributes=False)
teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser')
teaser_manual = fields.Text(string='Teaser Content')
diff --git a/addons/website_event_track/models/website_event_track.py b/addons/website_event_track/models/website_event_track.py
index 7f98f372e45..969bef0fa61 100644
--- a/addons/website_event_track/models/website_event_track.py
+++ b/addons/website_event_track/models/website_event_track.py
@@ -49,7 +49,7 @@ class Track(models.Model):
state = fields.Selection([
('draft', 'Proposal'), ('confirmed', 'Confirmed'), ('announced', 'Announced'), ('published', 'Published'), ('refused', 'Refused'), ('cancel', 'Cancelled')],
'Status', default='draft', required=True, copy=False, track_visibility='onchange')
- description = fields.Html('Track Description', translate=html_translate, sanitize=False)
+ description = fields.Html('Track Description', translate=html_translate, sanitize_attributes=False)
date = fields.Datetime('Track Date')
duration = fields.Float('Duration', default=1.5)
location_id = fields.Many2one('event.track.location', 'Room')
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index a1bdeb9fbde..3ef6636af73 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -711,7 +711,7 @@ class Post(models.Model):
question = self.parent_id
values = {
'author_id': self.sudo().create_uid.partner_id.id, # use sudo here because of access to res.users model
- 'body': tools.html_sanitize(self.content, strict=True, strip_style=True, strip_classes=True),
+ 'body': tools.html_sanitize(self.content, sanitize_attributes=True, strip_style=True, strip_classes=True),
'message_type': 'comment',
'subtype': 'mail.mt_comment',
'date': self.create_date,
diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py
index 4d77e37e459..1a0960a444e 100644
--- a/addons/website_hr_recruitment/models/hr_recruitment.py
+++ b/addons/website_hr_recruitment/models/hr_recruitment.py
@@ -47,7 +47,7 @@ class Job(models.Model):
default_description = self.env["ir.model.data"].xmlid_to_object("website_hr_recruitment.default_website_description")
return (default_description.render() if default_description else "")
- website_description = fields.Html('Website description', translate=html_translate, sanitize=False, default=_get_default_website_description)
+ website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description)
@api.multi
def _compute_website_url(self):
diff --git a/addons/website_livechat/models/im_livechat.py b/addons/website_livechat/models/im_livechat.py
index f73d3b594f7..17ad3f89a12 100644
--- a/addons/website_livechat/models/im_livechat.py
+++ b/addons/website_livechat/models/im_livechat.py
@@ -17,4 +17,4 @@ class ImLivechatChannel(models.Model):
for channel in self:
channel.website_url = "/livechat/channel/%s" % (slug(channel),)
- website_description = fields.Html("Website description", default=False, help="Description of the channel displayed on the website page", sanitize=False, translate=html_translate)
+ website_description = fields.Html("Website description", default=False, help="Description of the channel displayed on the website page", sanitize_attributes=False, translate=html_translate)
diff --git a/addons/website_mass_mailing/models/mass_mailing_list.py b/addons/website_mass_mailing/models/mass_mailing_list.py
index 7058e385b05..3e49bcba59b 100644
--- a/addons/website_mass_mailing/models/mass_mailing_list.py
+++ b/addons/website_mass_mailing/models/mass_mailing_list.py
@@ -18,6 +18,6 @@ class MassMailingList(models.Model):
</div>
<p class="o_message_paragraph">Join our Marketing newsletter and get <strong>this white paper instantly</strong></p>"""
- popup_content = fields.Html(string="Website Popup Content", translate=True, sanitize=False,
+ popup_content = fields.Html(string="Website Popup Content", translate=True, sanitize_attributes=False,
default=_default_popup_content)
popup_redirect_url = fields.Char(string="Website Popup Redirect URL", default='/')
diff --git a/addons/website_quote/models/product_template.py b/addons/website_quote/models/product_template.py
index ef2773225f1..55f3fbc1dec 100644
--- a/addons/website_quote/models/product_template.py
+++ b/addons/website_quote/models/product_template.py
@@ -7,5 +7,5 @@ from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
- website_description = fields.Html('Description for the website', sanitize=False) # hack, if website_sale is not installed
- quote_description = fields.Html('Description for the quote', sanitize=False)
+ website_description = fields.Html('Description for the website', sanitize_attributes=False) # hack, if website_sale is not installed
+ quote_description = fields.Html('Description for the quote', sanitize_attributes=False)
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index 513185d0075..4e069e2c916 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -56,7 +56,7 @@ class SaleOrder(models.Model):
'sale.quote.template', 'Quotation Template',
default=_get_default_template_id, readonly=True,
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
- website_description = fields.Html('Description', sanitize=False, translate=html_translate)
+ website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate)
options = fields.One2many(
'sale.order.option', 'order_id', 'Optional Products Lines',
copy=True, readonly=True,
@@ -199,7 +199,7 @@ class SaleOrderOption(models.Model):
name = fields.Text('Description', required=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
- website_description = fields.Html('Line Description', sanitize=False, translate=html_translate)
+ website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True)
diff --git a/addons/website_quote/models/sale_quote.py b/addons/website_quote/models/sale_quote.py
index 895c2930d89..817df1e087c 100644
--- a/addons/website_quote/models/sale_quote.py
+++ b/addons/website_quote/models/sale_quote.py
@@ -11,7 +11,7 @@ class SaleQuoteTemplate(models.Model):
_description = "Sale Quotation Template"
name = fields.Char('Quotation Template', required=True)
- website_description = fields.Html('Description', translate=html_translate, sanitize=False)
+ website_description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
quote_line = fields.One2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True)
note = fields.Text('Terms and conditions')
options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True)
@@ -100,7 +100,7 @@ class SaleQuoteOption(models.Model):
name = fields.Text('Description', required=True, translate=True)
product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True)
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
- website_description = fields.Html('Option Description', translate=html_translate, sanitize=False)
+ website_description = fields.Html('Option Description', translate=html_translate, sanitize_attributes=False)
price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True)
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index ef1096f6285..fe2bb8dcf64 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -81,7 +81,7 @@ class ProductTemplate(models.Model):
domain=lambda self: ['&', ('model', '=', self._name), ('message_type', '=', 'comment')],
string='Website Comments',
)
- website_description = fields.Html('Description for the website', sanitize=False, translate=True)
+ website_description = fields.Html('Description for the website', sanitize_attributes=False, translate=True)
alternative_product_ids = fields.Many2many('product.template', 'product_alternative_rel', 'src_id', 'dest_id',
string='Suggested Products', help='Appear on the product page')
accessory_product_ids = fields.Many2many('product.product', 'product_accessory_rel', 'src_id', 'dest_id',
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index b2e6fb0ae7f..602b95014e7 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -34,7 +34,7 @@ class Channel(models.Model):
name = fields.Char('Name', translate=True, required=True)
active = fields.Boolean(default=True)
- description = fields.Html('Description', translate=html_translate, sanitize=False)
+ description = fields.Html('Description', translate=html_translate, sanitize_attributes=False)
sequence = fields.Integer(default=10, help='Display order')
category_ids = fields.One2many('slide.category', 'channel_id', string="Categories")
slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides")
@@ -102,7 +102,7 @@ class Channel(models.Model):
string='Channel Groups', help="Groups allowed to see presentations in this channel")
access_error_msg = fields.Html(
'Error Message', help="Message to display when not accessible due to access rights",
- default="<p>This channel is private and its content is restricted to some users.</p>", translate=html_translate, sanitize=False)
+ default="<p>This channel is private and its content is restricted to some users.</p>", translate=html_translate, sanitize_attributes=False)
upload_group_ids = fields.Many2many(
'res.groups', 'rel_upload_groups', 'channel_id', 'group_id',
string='Upload Groups', help="Groups allowed to upload presentations in this channel. If void, every user can upload.")
diff --git a/openerp/addons/base/tests/test_mail.py b/openerp/addons/base/tests/test_mail.py
index a751b7a76e3..795d6824886 100644
--- a/openerp/addons/base/tests/test_mail.py
+++ b/openerp/addons/base/tests/test_mail.py
@@ -133,18 +133,25 @@ class TestSanitizer(unittest.TestCase):
]
for test, in_lst, out_lst in test_data:
- new_html = html_sanitize(test, strict=False, strip_style=False, strip_classes=False)
+ new_html = html_sanitize(test, sanitize_attributes=False, sanitize_style=True, strip_style=False, strip_classes=False)
for text in in_lst:
self.assertIn(text, new_html)
for text in out_lst:
self.assertNotIn(text, new_html)
# style should not be sanitized if removed
- new_html = html_sanitize(test_data[0][0], strict=False, strip_style=True, strip_classes=False)
+ new_html = html_sanitize(test_data[0][0], sanitize_attributes=False, strip_style=True, strip_classes=False)
self.assertEqual(new_html, u'<span>Coin coin </span>')
def test_style_class(self):
- html = html_sanitize(test_mail_examples.REMOVE_CLASS, strict=True, strip_style=False, strip_classes=True)
+ html = html_sanitize(test_mail_examples.REMOVE_CLASS, sanitize_attributes=True, sanitize_style=True, strip_classes=True)
+ for ext in test_mail_examples.REMOVE_CLASS_IN:
+ self.assertIn(ext, html)
+ for ext in test_mail_examples.REMOVE_CLASS_OUT:
+ self.assertNotIn(ext, html,)
+
+ def test_style_class_only(self):
+ html = html_sanitize(test_mail_examples.REMOVE_CLASS, sanitize_attributes=False, sanitize_style=True, strip_classes=True)
for ext in test_mail_examples.REMOVE_CLASS_IN:
self.assertIn(ext, html)
for ext in test_mail_examples.REMOVE_CLASS_OUT:
diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py
index bd0c9b250cb..e302c25e031 100644
--- a/openerp/addons/test_new_api/models.py
+++ b/openerp/addons/test_new_api/models.py
@@ -193,9 +193,9 @@ class MixedModel(models.Model):
reference = fields.Reference(string='Related Document',
selection='_reference_models')
comment1 = fields.Html(sanitize=False)
- comment2 = fields.Html(sanitize=True, strip_classes=False)
- comment3 = fields.Html(sanitize=True, strip_classes=True)
- comment4 = fields.Html(sanitize=True, strip_style=True)
+ comment2 = fields.Html(sanitize_attributes=True, strip_classes=False)
+ comment3 = fields.Html(sanitize_attributes=True, strip_classes=True)
+ comment4 = fields.Html(sanitize_attributes=True, strip_style=True)
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.ref('base.EUR'))
amount = fields.Monetary()
diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py
index 7cc018744f8..068d300abb1 100644
--- a/openerp/addons/test_new_api/tests/test_new_fields.py
+++ b/openerp/addons/test_new_api/tests/test_new_fields.py
@@ -605,9 +605,9 @@ class TestHtmlField(common.TransactionCase):
def test_00_sanitize(self):
self.assertEqual(self.model._fields['comment1'].sanitize, False)
- self.assertEqual(self.model._fields['comment2'].sanitize, True)
+ self.assertEqual(self.model._fields['comment2'].sanitize_attributes, True)
self.assertEqual(self.model._fields['comment2'].strip_classes, False)
- self.assertEqual(self.model._fields['comment3'].sanitize, True)
+ self.assertEqual(self.model._fields['comment3'].sanitize_attributes, True)
self.assertEqual(self.model._fields['comment3'].strip_classes, True)
some_ugly_html = """<p>Oops this should maybe be sanitized
diff --git a/openerp/fields.py b/openerp/fields.py
index 22b54e69be8..e25dbfdb884 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -1304,7 +1304,10 @@ class Html(_String):
type = 'html'
_slots = {
'sanitize': True, # whether value must be sanitized
- 'strip_style': False, # whether to strip style attributes (otherwise: sanitized)
+ 'sanitize_tags': True, # whether to sanitize tags (only a white list of attributes is accepted)
+ 'sanitize_attributes': True, # whether to sanitize attributes (only a white list of attributes is accepted)
+ 'sanitize_style': False, # whether to sanitize style attributes
+ 'strip_style': False, # whether to strip style attributes (removed and therefore not sanitized)
'strip_classes': False, # whether to strip classes attributes
}
@@ -1318,6 +1321,18 @@ class Html(_String):
_related_sanitize = property(attrgetter('sanitize'))
_description_sanitize = property(attrgetter('sanitize'))
+ _column_sanitize_tags = property(attrgetter('sanitize_tags'))
+ _related_sanitize_tags = property(attrgetter('sanitize_tags'))
+ _description_sanitize_tags = property(attrgetter('sanitize_tags'))
+
+ _column_sanitize_attributes = property(attrgetter('sanitize_attributes'))
+ _related_sanitize_attributes = property(attrgetter('sanitize_attributes'))
+ _description_sanitize_attributes = property(attrgetter('sanitize_attributes'))
+
+ _column_sanitize_style = property(attrgetter('sanitize_style'))
+ _related_sanitize_style = property(attrgetter('sanitize_style'))
+ _description_sanitize_style = property(attrgetter('sanitize_style'))
+
_column_strip_style = property(attrgetter('strip_style'))
_related_strip_style = property(attrgetter('strip_style'))
_description_strip_style = property(attrgetter('strip_style'))
@@ -1330,7 +1345,13 @@ class Html(_String):
if value is None or value is False:
return False
if validate and self.sanitize:
- return html_sanitize(value, silent=True, strict=True, strip_style=self.strip_style, strip_classes=self.strip_classes)
+ return html_sanitize(
+ value, silent=True,
+ sanitize_tags=self.sanitize_tags,
+ sanitize_attributes=self.sanitize_attributes,
+ sanitize_style=self.sanitize_style,
+ strip_style=self.strip_style,
+ strip_classes=self.strip_classes)
return value
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 413bea7bea0..8275efa4b4a 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -339,18 +339,26 @@ class text(_column):
class html(text):
_type = 'html'
_symbol_c = '%s'
- __slots__ = ['_sanitize', '_strip_style', '_strip_classes', '_symbol_f', '_symbol_set']
+ __slots__ = ['_sanitize', '_sanitize_tags', '_sanitize_attributes', '_sanitize_style', '_strip_style', '_strip_classes', '_symbol_f', '_symbol_set']
def _symbol_set_html(self, value):
if value is None or value is False:
return None
if not self._sanitize:
return value
- return html_sanitize(value, silent=True, strict=True, strip_style=self._strip_style, strip_classes=self._strip_classes)
-
- def __init__(self, string='unknown', sanitize=True, strip_style=False, strip_classes=False, **args):
+ return html_sanitize(
+ value, silent=True,
+ sanitize_attributes=self._sanitize_attributes,
+ sanitize_style=self._sanitize_style,
+ strip_style=self._strip_style,
+ strip_classes=self._strip_classes)
+
+ def __init__(self, string='unknown', sanitize=True, sanitize_tags=True, sanitize_attributes=True, sanitize_style=False, strip_style=False, strip_classes=False, **args):
super(html, self).__init__(string=string, **args)
self._sanitize = sanitize
+ self._sanitize_tags = sanitize_tags
+ self._sanitize_attributes = sanitize_attributes
+ self._sanitize_style = sanitize_style
self._strip_style = strip_style
self._strip_classes = strip_classes
# symbol_set redefinition because of sanitize specific behavior
@@ -360,6 +368,9 @@ class html(text):
def to_field_args(self):
args = super(html, self).to_field_args()
args['sanitize'] = self._sanitize
+ args['sanitize_tags'] = self._sanitize_tags
+ args['sanitize_attributes'] = self._sanitize_attributes
+ args['sanitize_style'] = self._sanitize_style
args['strip_style'] = self._strip_style
args['strip_classes'] = self._strip_classes
return args
diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py
index 3714df9c56f..2e432e92941 100644
--- a/openerp/tools/mail.py
+++ b/openerp/tools/mail.py
@@ -50,6 +50,9 @@ class _Cleaner(clean.Cleaner):
# tables
'border-collapse', 'border-spacing', 'caption-side', 'empty-cells', 'table-layout']
+ strip_classes = False
+ sanitize_style = False
+
def __call__(self, doc):
# perform quote detection before cleaning and class removal
for el in doc.iter():
@@ -57,8 +60,13 @@ class _Cleaner(clean.Cleaner):
super(_Cleaner, self).__call__(doc)
+ # if we keep attributes but still remove classes
+ if not getattr(self, 'safe_attrs_only', False) and self.strip_classes:
+ for el in doc.iter():
+ self.strip_class(el)
+
# if we keep style attribute, sanitize them
- if not self.style:
+ if not self.style and self.sanitize_style:
for el in doc.iter():
self.parse_style(el)
@@ -123,6 +131,10 @@ class _Cleaner(clean.Cleaner):
if el.getparent() is not None and (el.getparent().get('data-o-mail-quote') or el.getparent().get('data-o-mail-quote-container')) and not el.getparent().get('data-o-mail-quote-node'):
el.set('data-o-mail-quote', '1')
+ def strip_class(self, el):
+ if el.attrib.get('class'):
+ del el.attrib['class']
+
def parse_style(self, el):
attributes = el.attrib
styling = attributes.get('style')
@@ -143,7 +155,7 @@ class _Cleaner(clean.Cleaner):
return super(_Cleaner, self).allow_element(el)
-def html_sanitize(src, silent=True, strict=False, strip_style=False, strip_classes=False):
+def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=False, sanitize_style=False, strip_style=False, strip_classes=False):
if not src:
return src
src = ustr(src, errors='replace')
@@ -164,35 +176,38 @@ def html_sanitize(src, silent=True, strict=False, strip_style=False, strip_class
kwargs = {
'page_structure': True,
- 'style': strip_style, # True = remove style tags/attrs
- 'forms': True, # remove form tags
+ 'style': strip_style, # True = remove style tags/attrs
+ 'sanitize_style': sanitize_style, # True = sanitize styling
+ 'forms': True, # True = remove form tags
'remove_unknown_tags': False,
- 'allow_tags': allowed_tags,
'comments': False,
'processing_instructions': False
}
- if etree.LXML_VERSION >= (2, 3, 1):
- # kill_tags attribute has been added in version 2.3.1
- kwargs.update({
- 'kill_tags': tags_to_kill,
- 'remove_tags': tags_to_remove,
- })
- else:
- kwargs['remove_tags'] = tags_to_kill + tags_to_remove
-
- if strict:
- if etree.LXML_VERSION >= (3, 1, 0):
- # lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
- if strip_classes:
- current_safe_attrs = safe_attrs - frozenset(['class'])
- else:
- current_safe_attrs = safe_attrs
+ if sanitize_tags:
+ kwargs['allow_tags'] = allowed_tags
+ if etree.LXML_VERSION >= (2, 3, 1):
+ # kill_tags attribute has been added in version 2.3.1
kwargs.update({
- 'safe_attrs_only': True,
- 'safe_attrs': current_safe_attrs,
+ 'kill_tags': tags_to_kill,
+ 'remove_tags': tags_to_remove,
})
+ else:
+ kwargs['remove_tags'] = tags_to_kill + tags_to_remove
+
+ if sanitize_attributes and etree.LXML_VERSION >= (3, 1, 0): # lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
+ if strip_classes:
+ current_safe_attrs = safe_attrs - frozenset(['class'])
+ else:
+ current_safe_attrs = safe_attrs
+ kwargs.update({
+ 'safe_attrs_only': True,
+ 'safe_attrs': current_safe_attrs,
+ })
else:
- kwargs['safe_attrs_only'] = False # keep oe-data attributes + style
+ kwargs.update({
+ 'safe_attrs_only': False, # keep oe-data attributes + style
+ 'strip_classes': strip_classes, # remove classes, even when keeping other attributes
+ })
try:
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)