[IMP] tools.safe_eval_qweb: methods intended to provide more restricted alternatives...
[odoo/odoo.git] / openerp / tools / qweb.py
index 7b19666..abe60fb 100644 (file)
@@ -1,57 +1,15 @@
-import cgi
 import logging
-import types
+import re
 
-#from openerp.tools.safe_eval import safe_eval as eval
+import werkzeug.utils
+from openerp.tools.safe_eval_qweb import safe_eval_qweb as eval, UndefinedError, SecurityError
 
 import xml   # FIXME use lxml
-import xml.dom.minidom
+import traceback
+from openerp.osv import osv, orm
 
 _logger = logging.getLogger(__name__)
 
-class QWebEval(object):
-    def __init__(self, data):
-        self.data = data
-
-    def __getitem__(self, expr):
-        if expr in self.data:
-            return self.data[expr]
-        r = ''
-        try:
-            r = eval(expr, self.data)
-        except NameError:
-            pass
-        except AttributeError:
-            pass
-        except Exception:
-            _logger.exception('invalid expression: %r', expr)
-
-        self.data.pop('__builtins__', None)
-        return r
-
-    def eval_object(self, expr):
-        return self[expr]
-
-    def eval_str(self, expr):
-        if expr == "0":
-            return self.data.get(0, '')
-        if isinstance(self[expr], unicode):
-            return self[expr].encode("utf8")
-        return str(self[expr])
-
-    def eval_format(self, expr):
-        try:
-            return str(expr % self)
-        except:
-            return "qweb: format error '%s' " % expr
-#       if isinstance(r,unicode):
-#           return r.encode("utf8")
-
-    def eval_bool(self, expr):
-        if self.eval_object(expr):
-            return 1
-        else:
-            return 0
 
 class QWebXml(object):
     """QWeb Xml templating engine
@@ -69,12 +27,15 @@ class QWebXml(object):
 
 
     """
-    def __init__(self, loader):
+    def __init__(self, loader=None, undefined_handler=None):
         self.loader = loader
+        self.undefined_handler = undefined_handler
         self.node = xml.dom.Node
         self._t = {}
         self._render_tag = {}
-
+        self._format_regex = re.compile('(#\{(.*?)\})|(\{\{(.*?)\}\})')
+        self._void_elements = set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+                                  'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
         prefix = 'render_tag_'
         for i in [j for j in dir(self) if j.startswith(prefix)]:
             name = i[len(prefix):].replace('_', '-')
@@ -97,34 +58,74 @@ class QWebXml(object):
         else:
             dom = xml.dom.minidom.parse(x)
         for n in dom.documentElement.childNodes:
-            if n.nodeName == "t":
+            if n.nodeType == 1 and n.getAttribute('t-name'):
                 self._t[str(n.getAttribute("t-name"))] = n
 
     def get_template(self, name):
         if name in self._t:
             return self._t[name]
-        else:
+        elif self.loader:
             xml = self.loader(name)
             self.add_template(xml)
             if name in self._t:
                 return self._t[name]
         raise KeyError('qweb: template "%s" not found' % name)
 
+    def eval(self, expr, v):
+        try:
+            return eval(expr, None, v)
+        except (osv.except_osv, orm.except_orm), err:
+            raise orm.except_orm("QWeb Error", "Invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), err[1]))
+        except (UndefinedError, SecurityError), err:
+            if self.undefined_handler:
+                return self.undefined_handler(expr, v)
+            else:
+                raise SyntaxError(err.message)
+        except Exception:
+            raise SyntaxError("QWeb: invalid expression %r while rendering template '%s'.\n\n%s" % (expr, v.get('__template__'), traceback.format_exc()))
+
     def eval_object(self, expr, v):
-        return QWebEval(v).eval_object(expr)
+        return self.eval(expr, v)
 
     def eval_str(self, expr, v):
-        return QWebEval(v).eval_str(expr)
+        if expr == "0":
+            return v.get(0, '')
+        val = self.eval(expr, v)
+
+        if isinstance(val, unicode):
+            return val.encode("utf8")
+        return str(val)
 
     def eval_format(self, expr, v):
-        return QWebEval(v).eval_format(expr)
+        use_native = True
+        for m in self._format_regex.finditer(expr):
+            use_native = False
+            expr = expr.replace(m.group(), self.eval_str(m.groups()[1] or m.groups()[3], v))
+
+        if not use_native:
+            return expr
+        else:
+            try:
+                return str(expr % v)
+            except:
+                raise Exception("QWeb: format error '%s' " % expr)
 
     def eval_bool(self, expr, v):
-        return QWebEval(v).eval_bool(expr)
+        val = self.eval(expr, v)
+        if val:
+            return 1
+        else:
+            return 0
 
     def render(self, tname, v=None, out=None):
         if v is None:
             v = {}
+        v['__template__'] = tname
+        stack = v.get('__stack__', [])
+        if stack:
+            v['__caller__'] = stack[-1]
+        stack.append(tname)
+        v['__stack__'] = stack
         return self.render_node(self.get_template(tname), v)
 
     def render_node(self, e, v):
@@ -137,7 +138,7 @@ class QWebXml(object):
             t_att = {}
             for (an, av) in e.attributes.items():
                 an = str(an)
-                if isinstance(av, types.UnicodeType):
+                if isinstance(av, unicode):
                     av = av.encode("utf8")
                 else:
                     av = av.nodeValue.encode("utf8")
@@ -151,12 +152,18 @@ class QWebXml(object):
                             t_render = an[2:]
                         t_att[an[2:]] = av
                 else:
-                    g_att += ' %s="%s"' % (an, cgi.escape(av, 1))
+                    g_att += ' %s="%s"' % (an, werkzeug.utils.escape(av))
+
+            if 'debug' in t_att:
+                debugger = t_att.get('debug', 'pdb')
+                __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
             if t_render:
                 if t_render in self._render_tag:
                     r = self._render_tag[t_render](self, e, t_att, g_att, v)
             else:
                 r = self.render_element(e, t_att, g_att, v)
+        if isinstance(r, unicode):
+            return r.encode('utf-8')
         return r
 
     def render_element(self, e, t_att, g_att, v, inner=None):
@@ -184,9 +191,10 @@ class QWebXml(object):
             inner = inner.strip()
         if name == "t":
             return inner
-        elif len(inner) or name in ['script','i']:
-            # script should be rendered as <script></script>
-            return "<%s%s>%s</%s>" % (name, g_att, inner, name)
+        elif len(inner) or name not in self._void_elements:
+            return "<%s%s>%s</%s>" % tuple(
+                v if isinstance(v, str) else v.encode('utf-8')
+                for v in (name, g_att, inner, name))
         else:
             return "<%s%s/>" % (name, g_att)
 
@@ -195,27 +203,41 @@ class QWebXml(object):
         if an.startswith("t-attf-"):
             att, val = an[7:], self.eval_format(av, v)
         elif an.startswith("t-att-"):
-            att, val = an[6:], self.eval_str(av, v)
+            att, val = an[6:], self.eval(av, v)
+            if isinstance(val, unicode):
+                val = val.encode("utf8")
         else:
             att, val = self.eval_object(av, v)
-        return ' %s="%s"' % (att, cgi.escape(val, 1))
+        return val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
+
+    def render_att_href(self, e, an, av, v):
+        return self.url_for(e, an, av, v)
+    def render_att_src(self, e, an, av, v):
+        return self.url_for(e, an, av, v)
+    def render_att_action(self, e, an, av, v):
+        return self.url_for(e, an, av, v)
+    def url_for(self, e, an, av, v):
+        if 'url_for' not in v:
+            raise KeyError("qweb: no 'url_for' found in context")
+        path = str(v['url_for'](self.eval_format(av, v)))
+        return ' %s="%s"' % (an[2:], werkzeug.utils.escape(path))
 
     # Tags
     def render_tag_raw(self, e, t_att, g_att, v):
         inner = self.eval_str(t_att["raw"], v)
-        return self.render_element(e, t_att,  g_att, v, inner)
+        return self.render_element(e, t_att, g_att, v, inner)
 
     def render_tag_rawf(self, e, t_att, g_att, v):
         inner = self.eval_format(t_att["rawf"], v)
-        return self.render_element(e, t_att,  g_att, v, inner)
+        return self.render_element(e, t_att, g_att, v, inner)
 
     def render_tag_esc(self, e, t_att, g_att, v):
-        inner = cgi.escape(self.eval_str(t_att["esc"], v))
-        return self.render_element(e, t_att,  g_att, v, inner)
+        inner = werkzeug.utils.escape(self.eval_str(t_att["esc"], v))
+        return self.render_element(e, t_att, g_att, v, inner)
 
     def render_tag_escf(self, e, t_att, g_att, v):
-        inner = cgi.escape(self.eval_format(t_att["escf"], v))
-        return self.render_element(e, t_att,  g_att, v, inner)
+        inner = werkzeug.utils.escape(self.eval_format(t_att["escf"], v))
+        return self.render_element(e, t_att, g_att, v, inner)
 
     def render_tag_foreach(self, e, t_att, g_att, v):
         expr = t_att["foreach"]
@@ -224,9 +246,7 @@ class QWebXml(object):
             var = t_att.get('as', expr).replace('.', '_')
             d = v.copy()
             size = -1
-            if isinstance(enum, types.ListType):
-                size = len(enum)
-            elif isinstance(enum, types.TupleType):
+            if isinstance(enum, (list, tuple)):
                 size = len(enum)
             elif hasattr(enum, 'count'):
                 size = enum.count()
@@ -245,15 +265,15 @@ class QWebXml(object):
                     d["%s_parity" % var] = 'odd'
                 else:
                     d["%s_parity" % var] = 'even'
-                if isinstance(i, types.DictType):
-                    d.update(i)
-                else:
+                if 'as' in t_att:
                     d[var] = i
+                elif isinstance(i, dict):
+                    d.update(i)
                 ru.append(self.render_element(e, t_att, g_att, d))
                 index += 1
             return "".join(ru)
         else:
-            return "qweb: t-foreach %s not found." % expr
+            raise NameError("QWeb: foreach enumerator %r is not defined while rendering template %r" % (expr, v.get('__template__')))
 
     def render_tag_if(self, e, t_att, g_att, v):
         if self.eval_bool(t_att["if"], v):
@@ -267,35 +287,57 @@ class QWebXml(object):
         else:
             d = v.copy()
         d[0] = self.render_element(e, t_att, g_att, d)
-        return self.render(t_att["call"], d)
+        return self.render(self.eval_format(t_att["call"], d), d)
 
     def render_tag_set(self, e, t_att, g_att, v):
         if "value" in t_att:
             v[t_att["set"]] = self.eval_object(t_att["value"], v)
+        elif "valuef" in t_att:
+            v[t_att["set"]] = self.eval_format(t_att["valuef"], v)
         else:
             v[t_att["set"]] = self.render_element(e, t_att, g_att, v)
         return ""
 
     def render_tag_field(self, e, t_att, g_att, v):
         """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
-        record = v[t_att["record"]]
+        node_name = e.nodeName
+        assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
+                                 "ol", "ul", "ol", "dl", "dt", "dd"),\
+            "RTE widgets do not work correctly on %r elements" % node_name
+        assert node_name != 't',\
+            "t-field can not be used on a t element, provide an actual HTML node"
 
-        inner = ""
+        record, field = t_att["field"].rsplit('.', 1)
+        record = self.eval_object(record, v)
+
+        column = record._model._all_columns[field].column
+        field_type = column._type
+
+        req = v['request']
+        converter = req.registry['ir.fields.converter'].from_field(
+            req.cr, req.uid, record._model, column, totype='html')
+
+        content = None
         try:
-            if record._model._columns.get(t_att["field"])._type == 'many2one':
-                field = getattr(record, t_att["field"])
-                if field:
-                    inner = cgi.escape(str(field.name_get()[0][1]))
-            else:
-                inner = cgi.escape(str(getattr(record, t_att["field"])))
-            if e.tagName != 't':
-                # <t/> are escaped
-                g_att += ' %s="%s"' % ('data-oe-model', record._model._name)
-                g_att += ' %s="%s"' % ('data-oe-id', str(record.id))
-                g_att += ' %s="%s"' % ('data-oe-field', t_att["field"])
-        except AttributeError:
-            _logger.warning("t-field no field %s for model %s", t_att["field"], record._model._name)
-
-        return self.render_element(e, t_att,  g_att, v, inner)
+            value = record[field]
+            if value:
+                content, warnings = converter(value)
+                assert not warnings
+        except KeyError:
+            _logger.warning("t-field no field %s for model %s", field, record._model._name)
+
+        g_att += ''.join(
+            ' %s="%s"' % (name, werkzeug.utils.escape(value))
+            for name, value in [
+                ('data-oe-model', record._model._name),
+                ('data-oe-id', record.id),
+                ('data-oe-field', field),
+                ('data-oe-type', field_type),
+                ('data-oe-translate', '1' if column.translate else '0'),
+                ('data-oe-expression', t_att['field']),
+            ]
+        )
+
+        return self.render_element(e, t_att, g_att, v, content or "")
 
 # leave this, al.