diff --git a/ext/odoo/odoo/addons/base/ir/ir_qweb/assetsbundle.py b/ext/odoo/odoo/addons/base/ir/ir_qweb/assetsbundle.py index 9c3b694a..059adda9 100644 --- a/ext/odoo/odoo/addons/base/ir/ir_qweb/assetsbundle.py +++ b/ext/odoo/odoo/addons/base/ir/ir_qweb/assetsbundle.py @@ -9,9 +9,12 @@ import textwrap import uuid from datetime import datetime from subprocess import Popen, PIPE +from collections import OrderedDict from odoo import fields, tools +from odoo.tools.pycompat import string_types, to_text from odoo.http import request from odoo.modules.module import get_resource_path +from odoo.addons.base.ir.ir_qweb.qweb import escape import psycopg2 from odoo.tools import func, misc @@ -65,7 +68,6 @@ def rjsmin(script): ).strip() return result - class AssetError(Exception): pass @@ -79,17 +81,16 @@ class AssetsBundle(object): rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""") rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/") - def __init__(self, name, files, remains, env=None): + # remains attribute is depreciated and will remove after v11 + def __init__(self, name, files, remains=None, env=None): self.name = name self.env = request.env if env is None else env self.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES) self.javascripts = [] self.stylesheets = [] self.css_errors = [] - self.remains = [] self._checksum = None self.files = files - self.remains = remains for f in files: if f['atype'] == 'text/sass': self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) @@ -100,10 +101,37 @@ class AssetsBundle(object): elif f['atype'] == 'text/javascript': self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])) - def to_html(self, sep=None, css=True, js=True, debug=False, async=False, url_for=(lambda url: url)): + # depreciated and will remove after v11 + def to_html(self, sep=None, css=True, js=True, debug=False, async_load=False, url_for=(lambda url: url), **kw): + if 'async' in kw: + _logger.warning("Using deprecated argument 'async' in to_html call, use 'async_load' instead.") + async_load = kw['async'] + nodes = self.to_node(css=css, js=js, debug=debug, async_load=async_load) + if sep is None: sep = u'\n ' response = [] + for tagName, attributes, content in nodes: + html = u"<%s " % tagName + for name, value in attributes.items(): + if value or isinstance(value, string_types): + html += u' %s="%s"' % (name, escape(to_text(value))) + if content is None: + html += u'/>' + else: + html += u'>%s' % (escape(to_text(content)), tagName) + response.append(html) + + return sep + sep.join(response) + + def to_node(self, css=True, js=True, debug=False, async_load=False, **kw): + """ + :returns [(tagName, attributes, content)] if the tag is auto close + """ + if 'async' in kw: + _logger.warning("Using deprecated argument 'async' in to_node call, use 'async_load' instead.") + async_load = kw['async'] + response = [] if debug == 'assets': if css and self.stylesheets: is_css_preprocessed, old_attachments = self.is_css_preprocessed() @@ -111,28 +139,37 @@ class AssetsBundle(object): self.preprocess_css(debug=debug, old_attachments=old_attachments) if self.css_errors: msg = '\n'.join(self.css_errors) - response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) - response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_html()) + response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node()) + response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/css/bootstrap.css").to_node()) if not self.css_errors: for style in self.stylesheets: - response.append(style.to_html()) + response.append(style.to_node()) if js: for jscript in self.javascripts: - response.append(jscript.to_html()) + response.append(jscript.to_node()) else: if css and self.stylesheets: css_attachments = self.css() or [] for attachment in css_attachments: - response.append(u'' % url_for(attachment.url)) + attr = OrderedDict([ + ["type", "text/css"], + ["rel", "stylesheet"], + ["href", attachment.url], + ]) + response.append(("link", attr, None)) if self.css_errors: msg = '\n'.join(self.css_errors) - response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_html()) + response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node()) if js and self.javascripts: - response.append(u'' % (async and u'async="async"' or '', url_for(self.js().url))) - response.extend(self.remains) + attr = OrderedDict([ + ["async", "async" if async_load else None], + ["type", "text/javascript"], + ["src", self.js().url], + ]) + response.append(("script", attr, None)) - return sep + sep.join(response) + return response @func.lazy_property def last_modified(self): @@ -152,17 +189,15 @@ class AssetsBundle(object): Not really a full checksum. We compute a SHA1 on the rendered bundle + max linked files last_modified date """ - check = u"%s%s%s" % (json.dumps(self.files, sort_keys=True), u",".join(self.remains), self.last_modified) + check = u"%s%s" % (json.dumps(self.files, sort_keys=True), self.last_modified) return hashlib.sha1(check.encode('utf-8')).hexdigest() def clean_attachments(self, type): """ Takes care of deleting any outdated ir.attachment records associated to a bundle before saving a fresh one. - When `type` is css we need to check that we are deleting a different version (and not *any* version) because css may be paginated and, therefore, may produce multiple attachments for the same bundle's version. - When `type` is js we need to check that we are deleting a different version (and not *any* version) because, as one of the creates in `save_attachment` can trigger a rollback, the call to `clean_attachments ` is made at the end of the method in order to avoid the rollback @@ -278,22 +313,16 @@ class AssetsBundle(object): (function (message) { if (window.__assetsBundleErrorSeen) return; window.__assetsBundleErrorSeen = true; - document.addEventListener("DOMContentLoaded", function () { var alertTimeout = setTimeout(alert.bind(window, message), 0); if (typeof odoo === "undefined") return; - odoo.define("AssetsBundle.ErrorMessage", function (require) { "use strict"; - var base = require("web_editor.base"); var core = require("web.core"); var Dialog = require("web.Dialog"); - var _t = core._t; - clearTimeout(alertTimeout); - base.ready().then(function () { new Dialog(null, { title: _t("Style error"), @@ -314,7 +343,7 @@ class AssetsBundle(object): outdated = False assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype)) if assets: - assets_domain = [('url', 'in', list(assets))] + assets_domain = [('url', 'in', list(assets.keys()))] attachments = self.env['ir.attachment'].sudo().search(assets_domain) for attachment in attachments: asset = assets[attachment.url] @@ -474,7 +503,20 @@ class WebAsset(object): except Exception: raise AssetNotFound("Could not find %s" % self.name) + # depreciated and will remove after v11 def to_html(self): + tagName, attributes, content = self.to_node() + html = u"<%s " % tagName + for name, value in attributes.items(): + if value or isinstance(value, string_types): + html += u' %s="%s"' % (name, escape(to_text(value))) + if content is None: + html += u'/>' + else: + html += u'>%s' % (escape(to_text(content)), tagName) + return html + + def to_node(self): raise NotImplementedError() @func.lazy_property @@ -533,13 +575,19 @@ class JavascriptAsset(WebAsset): try: return super(JavascriptAsset, self)._fetch_content() except AssetError as e: - return "console.error(%s);" % json.dumps(str(e)) + return u"console.error(%s);" % json.dumps(to_text(e)) - def to_html(self): + def to_node(self): if self.url: - return '' % (self.html_url) + return ("script", OrderedDict([ + ["type", "text/javascript"], + ["src", self.html_url], + ]), None) else: - return '' % self.with_header() + return ("script", OrderedDict([ + ["type", "text/javascript"], + ["charset", "utf-8"], + ]), self.with_header()) class StylesheetAsset(WebAsset): @@ -595,13 +643,21 @@ class StylesheetAsset(WebAsset): content = re.sub(r' *([{}]) *', r'\1', content) return self.with_header(content) - def to_html(self): - media = (' media="%s"' % misc.html_escape(self.media)) if self.media else '' + def to_node(self): if self.url: - href = self.html_url - return '' % (href, media) + attr = OrderedDict([ + ["type", "text/css"], + ["rel", "stylesheet"], + ["href", self.html_url], + ["media", escape(to_text(self.media)) if self.media else None] + ]) + return ("link", attr, None) else: - return '' % (media, self.with_header()) + attr = OrderedDict([ + ["type", "text/css"], + ["media", escape(to_text(self.media)) if self.media else None] + ]) + return ("style", attr, self.with_header()) class PreprocessedCSS(StylesheetAsset): @@ -666,4 +722,4 @@ class LessStylesheetAsset(PreprocessedCSS): except IOError: lessc = 'lessc' lesspath = get_resource_path('web', 'static', 'lib', 'bootstrap', 'less') - return [lessc, '-', '--no-js', '--no-color', '--include-path=%s' % lesspath] + return [lessc, '-', '--no-js', '--no-color', '--include-path=%s' % lesspath] \ No newline at end of file diff --git a/ext/odoo/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/ext/odoo/odoo/addons/base/ir/ir_qweb/ir_qweb.py index 8bb333ea..4b0e1633 100644 --- a/ext/odoo/odoo/addons/base/ir/ir_qweb/ir_qweb.py +++ b/ext/odoo/odoo/addons/base/ir/ir_qweb/ir_qweb.py @@ -37,9 +37,7 @@ class IrQWeb(models.AbstractModel, QWeb): @api.model def render(self, id_or_xml_id, values=None, **options): """ render(id_or_xml_id, values, **options) - Render the template specified by the given name. - :param id_or_xml_id: name or etree (see get_template) :param dict values: template values to be used for rendering :param options: used to compile the template (the dict available for the rendering is frozen) @@ -125,35 +123,122 @@ class IrQWeb(models.AbstractModel, QWeb): if len(el): raise SyntaxError("t-call-assets cannot contain children nodes") - # self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values) + # nodes = self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values) + # + # for index, (tagName, t_attrs, content) in enumerate(nodes): + # if index: + # append('\n ') + # append('<') + # append(tagName) + # + # self._post_processing_att(tagName, t_attrs, options) + # for name, value in t_attrs.items(): + # if value or isinstance(value, string_types)): + # append(u' ') + # append(name) + # append(u'="') + # append(escape(pycompat.to_text((value))) + # append(u'"') + # + # if not content and tagName in self._void_elements: + # append('/>') + # else: + # append('>') + # if content: + # append(content) + # append('') + # + space = el.getprevious() is not None and el.getprevious().tail or el.getparent().text + sep = u'\n' + space.rsplit('\n').pop() return [ - self._append(ast.Call( - func=ast.Attribute( - value=ast.Name(id='self', ctx=ast.Load()), - attr='_get_asset', - ctx=ast.Load() + ast.Assign( + targets=[ast.Name(id='nodes', ctx=ast.Store())], + value=ast.Call( + func=ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='_get_asset_nodes', + ctx=ast.Load() + ), + args=[ + ast.Str(el.get('t-call-assets')), + ast.Name(id='options', ctx=ast.Load()), + ], + keywords=[ + ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), + ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), + ast.keyword('debug', ast.Call( + func=ast.Attribute( + value=ast.Name(id='values', ctx=ast.Load()), + attr='get', + ctx=ast.Load() + ), + args=[ast.Str('debug')], + keywords=[], starargs=None, kwargs=None + )), + ast.keyword('async', self._get_attr_bool(el.get('async', False))), + ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), + ], + starargs=None, kwargs=None + ) + ), + ast.For( + target=ast.Tuple(elts=[ + ast.Name(id='index', ctx=ast.Store()), + ast.Tuple(elts=[ + ast.Name(id='tagName', ctx=ast.Store()), + ast.Name(id='t_attrs', ctx=ast.Store()), + ast.Name(id='content', ctx=ast.Store()) + ], ctx=ast.Store()) + ], ctx=ast.Store()), + iter=ast.Call( + func=ast.Name(id='enumerate', ctx=ast.Load()), + args=[ast.Name(id='nodes', ctx=ast.Load())], + keywords=[], + starargs=None, kwargs=None ), - args=[ - ast.Str(el.get('t-call-assets')), - ast.Name(id='options', ctx=ast.Load()), - ], - keywords=[ - ast.keyword('css', self._get_attr_bool(el.get('t-css', True))), - ast.keyword('js', self._get_attr_bool(el.get('t-js', True))), - ast.keyword('debug', ast.Call( - func=ast.Attribute( - value=ast.Name(id='values', ctx=ast.Load()), - attr='get', - ctx=ast.Load() + body=[ + ast.If( + test=ast.Name(id='index', ctx=ast.Load()), + body=[self._append(ast.Str(sep))], + orelse=[] + ), + self._append(ast.Str(u'<')), + self._append(ast.Name(id='tagName', ctx=ast.Load())), + ] + self._append_attributes() + [ + ast.If( + test=ast.BoolOp( + op=ast.And(), + values=[ + ast.UnaryOp(ast.Not(), ast.Name(id='content', ctx=ast.Load()), lineno=0, col_offset=0), + ast.Compare( + left=ast.Name(id='tagName', ctx=ast.Load()), + ops=[ast.In()], + comparators=[ast.Attribute( + value=ast.Name(id='self', ctx=ast.Load()), + attr='_void_elements', + ctx=ast.Load() + )] + ), + ] ), - args=[ast.Str('debug')], - keywords=[], starargs=None, kwargs=None - )), - ast.keyword('async', self._get_attr_bool(el.get('async', False))), - ast.keyword('values', ast.Name(id='values', ctx=ast.Load())), + body=[self._append(ast.Str(u'/>'))], + orelse=[ + self._append(ast.Str(u'>')), + ast.If( + test=ast.Name(id='content', ctx=ast.Load()), + body=[self._append(ast.Name(id='content', ctx=ast.Load()))], + orelse=[] + ), + self._append(ast.Str(u'')), + ] + ) ], - starargs=None, kwargs=None - )) + orelse=[] + ) ] # for backward compatibility to remove after v10 @@ -185,16 +270,34 @@ class IrQWeb(models.AbstractModel, QWeb): # method called by computing code + def get_asset_bundle(self, xmlid, files, remains=None, env=None): + return AssetsBundle(xmlid, files, remains=remains, env=env) + + # compatibility to remove after v11 - DEPRECATED + @tools.conditional( + 'xml' not in tools.config['dev_mode'], + tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)), + ) + def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw): + if 'async' in kw: + async_load = kw['async'] + files, remains = self._get_asset_content(xmlid, options) + asset = self.get_asset_bundle(xmlid, files, remains, env=self.env) + return asset.to_html(css=css, js=js, debug=debug, async_load=async_load, url_for=(values or {}).get('url_for', lambda url: url)) + @tools.conditional( # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools) 'xml' not in tools.config['dev_mode'], - tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)), + tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)), ) - def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): + def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw): + if 'async' in kw: + async_load = kw['async'] files, remains = self._get_asset_content(xmlid, options) - asset = AssetsBundle(xmlid, files, remains, env=self.env) - return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url)) + asset = self.get_asset_bundle(xmlid, files, env=self.env) + remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')] + return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load) @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",)) def _get_asset_content(self, xmlid, options): @@ -205,6 +308,9 @@ class IrQWeb(models.AbstractModel, QWeb): env = self.env(context=options) + def can_aggregate(url): + return not urls.url_parse(url).scheme and not urls.url_parse(url).netloc and not url.startswith('/web/content') + # TODO: This helper can be used by any template that wants to embedd the backend. # It is currently necessary because the ir.ui.view bundle inheritance does not # match the module dependency graph. @@ -218,16 +324,13 @@ class IrQWeb(models.AbstractModel, QWeb): files = [] remains = [] for el in html.fragments_fromstring(template): - if isinstance(el, pycompat.string_types): - remains.append(pycompat.to_text(el)) - elif isinstance(el, html.HtmlElement): + if isinstance(el, html.HtmlElement): href = el.get('href', '') src = el.get('src', '') atype = el.get('type') media = el.get('media') - can_aggregate = not urls.url_parse(href).netloc and not href.startswith('/web/content') - if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate): + if can_aggregate(href) and (el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet')): if href.endswith('.sass'): atype = 'text/sass' elif href.endswith('.less'): @@ -237,25 +340,26 @@ class IrQWeb(models.AbstractModel, QWeb): path = [segment for segment in href.split('/') if segment] filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': href, 'filename': filename, 'content': el.text, 'media': media}) - elif el.tag == 'script': + elif can_aggregate(src) and el.tag == 'script': atype = 'text/javascript' - path = [segment for segment in src.split('/') if segment] + path = [segment for segment in href.split('/') if segment] filename = get_resource_path(*path) if path else None files.append({'atype': atype, 'url': src, 'filename': filename, 'content': el.text, 'media': media}) else: - remains.append(html.tostring(el, encoding='unicode')) + remains.append((el.tag, OrderedDict(el.attrib), el.text)) else: - try: - remains.append(html.tostring(el, encoding='unicode')) - except Exception: - # notYETimplementederror - raise NotImplementedError + # the other cases are ignored + pass return (files, remains) def _get_field(self, record, field_name, expression, tagName, field_options, options, values): field = record._fields[field_name] + # adds template compile options for rendering fields + field_options['template_options'] = options + + # adds generic field options field_options['tagName'] = tagName field_options['expression'] = expression field_options['type'] = field_options.get('widget', field.type) @@ -275,6 +379,9 @@ class IrQWeb(models.AbstractModel, QWeb): return (attributes, content, inherit_branding or translate) def _get_widget(self, value, expression, tagName, field_options, options, values): + # adds template compile options for rendering fields + field_options['template_options'] = options + field_options['type'] = field_options['widget'] field_options['tagName'] = tagName field_options['expression'] = expression @@ -319,4 +426,4 @@ class IrQWeb(models.AbstractModel, QWeb): return ast.Name(id='False', ctx=ast.Load()) elif attr in ('true', '1'): return ast.Name(id='True', ctx=ast.Load()) - return ast.Name(id=str(attr if attr is False else default), ctx=ast.Load()) + return ast.Name(id=str(attr if attr is False else default), ctx=ast.Load()) \ No newline at end of file diff --git a/ext/odoo/odoo/tools/safe_eval.py b/ext/odoo/odoo/tools/safe_eval.py index 7f5d6e35..7a5ec037 100644 --- a/ext/odoo/odoo/tools/safe_eval.py +++ b/ext/odoo/odoo/tools/safe_eval.py @@ -101,6 +101,8 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [ 'CALL_FUNCTION_EX', # Already in P2 but apparently the first one is used more aggressively in P3 'CALL_FUNCTION_KW', 'CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW', + # Added in P3.7 https://bugs.python.org/issue26110 + 'CALL_METHOD', 'LOAD_METHOD', 'GET_ITER', 'FOR_ITER', 'YIELD_VALUE', 'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE', # New in Python 2.7 - http://bugs.python.org/issue4715 :