diff --git a/doc/06_misc_qweb.rst b/doc/06_misc_qweb.rst
index 227ea211c92..4f3284d913f 100644
--- a/doc/06_misc_qweb.rst
+++ b/doc/06_misc_qweb.rst
@@ -22,6 +22,13 @@ column, but this can be overridden by providing a ``widget`` as field option.
Field options are specified through ``t-field-options``, which must be a JSON
object (map). Custom widgets may define their own (possibly mandatory) options.
+Global options
+--------------
+
+A global option ``html-escape`` is provided. It defaults to ``True``, and for
+many (not all) fields it determines whether the field's output will be
+html-escaped before being output.
+
Date and datetime converters
----------------------------
@@ -88,3 +95,4 @@ The duration must be a positive number, and no rounding is applied.
.. _ldml date format patterns:
http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
+
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index ef9e8ba4de5..538be98c31c 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -530,7 +530,7 @@ class FieldConverter(osv.AbstractModel):
""" Converts a single value to its HTML version/output
"""
if not value: return ''
- return werkzeug.utils.escape(value)
+ return value
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
""" Converts the specified field of the browse_record ``record`` to
@@ -554,6 +554,10 @@ class FieldConverter(osv.AbstractModel):
cr, uid, field_name, record,
record._model._all_columns[field_name].column,
options, context=context)
+ if options.get('html-escape', True):
+ content = werkzeug.utils.escape(content)
+ elif hasattr(content, '__html__'):
+ content = content.__html__()
except Exception:
_logger.warning("Could not get field %s for model %s",
field_name, record._model._name, exc_info=True)
@@ -620,7 +624,7 @@ class FloatConverter(osv.AbstractModel):
# strip trailing 0.
if not precision:
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
- return werkzeug.utils.escape(formatted)
+ return formatted
class DateConverter(osv.AbstractModel):
_name = 'ir.qweb.field.date'
@@ -677,7 +681,8 @@ class TextConverter(osv.AbstractModel):
Escapes the value and converts newlines to br. This is bullshit.
"""
if not value: return ''
- return werkzeug.utils.escape(value).replace('\n', '
\n')
+
+ return nl2br(value, options=options)
class SelectionConverter(osv.AbstractModel):
_name = 'ir.qweb.field.selection'
@@ -699,14 +704,14 @@ class ManyToOneConverter(osv.AbstractModel):
[read] = record.read([field_name])
_, value = read[field_name]
- return werkzeug.utils.escape(value).replace('\n', '
\n')
+ return nl2br(value, options=options)
class HTMLConverter(osv.AbstractModel):
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, column, options=None, context=None):
- return value or ''
+ return HTMLSafe(value or '')
class ImageConverter(osv.AbstractModel):
""" ``image`` widget rendering, inserts a data:uri-using image tag in the
@@ -729,7 +734,7 @@ class ImageConverter(osv.AbstractModel):
except: # image.verify() throws "suitable exceptions", I have no idea what they are
raise ValueError("Invalid image content")
- return '' % (Image.MIME[image.format], value)
+ return HTMLSafe('' % (Image.MIME[image.format], value))
class MonetaryConverter(osv.AbstractModel):
""" ``monetary`` converter, has a mandatory option
@@ -783,12 +788,12 @@ class MonetaryConverter(osv.AbstractModel):
else:
post = u' {symbol}'
- return u'{pre}{0}{post}'.format(
+ return HTMLSafe(u'{pre}{0}{post}'.format(
formatted_amount,
pre=pre, post=post,
).format(
symbol=display.symbol,
- )
+ ))
def display_currency(self, cr, uid, options):
return self.qweb_object().eval_object(
@@ -858,6 +863,43 @@ class RelativeDatetimeConverter(osv.AbstractModel):
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
+class HTMLSafe(object):
+ """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
+ objects with a ``__html__`` methods but AFAIK does not provide any such
+ object.
+
+ Wrapping a string in HTML will prevent its escaping
+ """
+ __slots__ = ['string']
+ def __init__(self, string):
+ self.string = string
+ def __html__(self):
+ return self.string
+ def __str__(self):
+ s = self.string
+ if isinstance(s, unicode):
+ return s.encode('utf-8')
+ return s
+ def __unicode__(self):
+ s = self.string
+ if isinstance(s, str):
+ return s.decode('utf-8')
+ return s
+
+def nl2br(string, options=None):
+ """ Converts newlines to HTML linebreaks in ``string``. Automatically
+ escapes content unless options['html-escape'] is set to False, and returns
+ the result wrapped in an HTMLSafe object.
+
+ :param str string:
+ :param dict options:
+ :rtype: HTMLSafe
+ """
+ if options is None: options = {}
+
+ if options.get('html-escape', True):
+ string = werkzeug.utils.escape(string)
+ return HTMLSafe(string.replace('\n', '
\n'))
def get_field_type(column, options):
""" Gets a t-field's effective type from the field's column and its options
diff --git a/openerp/tests/addons/test_converter/tests/test_html.py b/openerp/tests/addons/test_converter/tests/test_html.py
index b6f787d647a..2f0896990ea 100644
--- a/openerp/tests/addons/test_converter/tests/test_html.py
+++ b/openerp/tests/addons/test_converter/tests/test_html.py
@@ -4,6 +4,8 @@ import os
import xml.dom.minidom
import datetime
+from werkzeug.utils import escape as e
+
from openerp.tests import common
from openerp.addons.base.ir import ir_qweb
@@ -36,8 +38,8 @@ class TestExport(common.TransactionCase):
break
except KeyError: pass
- return lambda value, options=None, context=None: model.value_to_html(
- self.cr, self.uid, value, column, options=options, context=context)
+ return lambda value, options=None, context=None: e(model.value_to_html(
+ self.cr, self.uid, value, column, options=options, context=context))
class TestBasicExport(TestExport):
_model = 'test_converter.test_model'
@@ -223,8 +225,8 @@ class TestMany2OneExport(TestBasicExport):
column = self.get_column('many2one')
model = self.registry('ir.qweb.field.many2one')
- return model.record_to_html(
- self.cr, self.uid, 'many2one', record, column)
+ return e(model.record_to_html(
+ self.cr, self.uid, 'many2one', record, column))
value = converter(self.Model.browse(self.cr, self.uid, id0))
self.assertEqual(value, "Foo")
@@ -241,8 +243,8 @@ class TestBinaryExport(TestBasicExport):
content = f.read()
encoded_content = content.encode('base64')
- value = converter.value_to_html(
- self.cr, self.uid, encoded_content, column)
+ value = e(converter.value_to_html(
+ self.cr, self.uid, encoded_content, column))
self.assertEqual(
value, '' % (
encoded_content
@@ -252,15 +254,15 @@ class TestBinaryExport(TestBasicExport):
content = f.read()
with self.assertRaises(ValueError):
- converter.value_to_html(
- self.cr, self.uid, 'binary', content.encode('base64'), column)
+ e(converter.value_to_html(
+ self.cr, self.uid, 'binary', content.encode('base64'), column))
with open(os.path.join(directory, 'test_vectors', 'pptx'), 'rb') as f:
content = f.read()
with self.assertRaises(ValueError):
- converter.value_to_html(
- self.cr, self.uid, 'binary', content.encode('base64'), column)
+ e(converter.value_to_html(
+ self.cr, self.uid, 'binary', content.encode('base64'), column))
class TestSelectionExport(TestBasicExport):
def test_selection(self):