123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- import os
- import re
- import logging
- from lxml import etree, html
- from subprocess import Popen, PIPE
- import openerp
- from openerp.osv import orm
- from openerp.tools import which
- from openerp.addons.base.ir.ir_qweb import AssetError, JavascriptAsset, QWebException
- _logger = logging.getLogger(__name__)
- class QWeb_less(orm.AbstractModel):
- _name = "website.qweb"
- _inherit = "website.qweb"
- def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
- """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
- if len(element):
- # An asset bundle is rendered in two differents contexts (when genereting html and
- # when generating the bundle itself) so they must be qwebcontext free
- # even '0' variable is forbidden
- template = qwebcontext.get('__template__')
- raise QWebException("t-call-assets cannot contain children nodes", template=template)
- xmlid = template_attributes['call-assets']
- cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
- bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
- css = self.get_attr_bool(template_attributes.get('css'), default=True)
- js = self.get_attr_bool(template_attributes.get('js'), default=True)
- return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
- def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext):
- d = qwebcontext.copy()
- d[0] = self.render_element(element, template_attributes, generated_attributes, d)
- cr = d.get('request') and d['request'].cr or None
- uid = d.get('request') and d['request'].uid or None
- template = self.eval_format(template_attributes["snippet"], d)
- document = self.render(cr, uid, template, d)
- node = etree.fromstring(document)
- id = int(node.get('data-oe-id', self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, template, raise_if_not_found=True)))
- node.set('data-oe-name', self.pool['ir.ui.view'].browse(cr, uid, id, context=qwebcontext.context).name)
- node.set('data-oe-type', "snippet")
- node.set('data-oe-thumbnail', template_attributes.get('thumbnail', "oe-thumbnail"))
- return html.tostring(node, encoding='UTF-8')
- class AssetsBundle(openerp.addons.base.ir.ir_qweb.AssetsBundle):
- rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
- def parse(self):
- fragments = html.fragments_fromstring(self.html)
- for el in fragments:
- if isinstance(el, basestring):
- self.remains.append(el)
- elif isinstance(el, html.HtmlElement):
- src = el.get('src', '')
- href = el.get('href', '')
- atype = el.get('type')
- media = el.get('media')
- if el.tag == 'style':
- if atype == 'text/sass' or src.endswith('.sass'):
- self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
- elif atype == 'text/less' or src.endswith('.less'):
- self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
- else:
- self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
- elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
- if href.endswith('.sass') or atype == 'text/sass':
- self.stylesheets.append(SassAsset(self, url=href, media=media))
- elif href.endswith('.less') or atype == 'text/less':
- self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
- else:
- self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
- elif el.tag == 'script' and not src:
- self.javascripts.append(JavascriptAsset(self, inline=el.text))
- elif el.tag == 'script' and self.can_aggregate(src):
- self.javascripts.append(JavascriptAsset(self, url=src))
- else:
- self.remains.append(html.tostring(el))
- else:
- try:
- self.remains.append(html.tostring(el))
- except Exception:
- # notYETimplementederror
- raise NotImplementedError
- def to_html(self, sep=None, css=True, js=True, debug=False):
- if sep is None:
- sep = '\n '
- response = []
- if debug:
- if css and self.stylesheets:
- self.preprocess_css()
- if self.css_errors:
- msg = '\n'.join(self.css_errors)
- self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
- for style in self.stylesheets:
- response.append(style.to_html())
- if js:
- for jscript in self.javascripts:
- response.append(jscript.to_html())
- else:
- url_for = self.context.get('url_for', lambda url: url)
- if css and self.stylesheets:
- href = '/web/css/%s/%s' % (self.xmlid, self.version)
- response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
- if js:
- src = '/web/js/%s/%s' % (self.xmlid, self.version)
- response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
- response.extend(self.remains)
- return sep + sep.join(response)
- def css(self):
- content = self.get_cache('css')
- if content is None:
- content = self.preprocess_css()
- if self.css_errors:
- msg = '\n'.join(self.css_errors)
- content += self.css_message(msg)
- # move up all @import rules to the top
- matches = []
- def push(matchobj):
- matches.append(matchobj.group(0))
- return ''
- content = re.sub(self.rx_css_import, push, content)
- matches.append(content)
- content = u'\n'.join(matches)
- if self.css_errors:
- return content
- self.set_cache('css', content)
- return content
- def set_cache(self, type, content):
- ira = self.registry['ir.attachment']
- ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
- url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
- ira.create(self.cr, openerp.SUPERUSER_ID, dict(
- datas=content.encode('utf8').encode('base64'),
- type='binary',
- name=url,
- url=url,
- ), context=self.context)
- def css_message(self, message):
- # '\A' == css content carriage return
- message = message.replace('\n', '\\A ').replace('"', '\\"')
- return """
- body:before {
- background: #ffc;
- width: 100%%;
- font-size: 14px;
- font-family: monospace;
- white-space: pre;
- content: "%s";
- }
- """ % message
- def preprocess_css(self):
- """
- Checks if the bundle contains any sass/less content, then compiles it to css.
- Returns the bundle's flat css.
- """
- for atype in (SassAsset, LessStylesheetAsset):
- assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
- if assets:
- cmd = assets[0].get_command()
- source = '\n'.join([asset.get_source() for asset in assets])
- compiled = self.compile_css(cmd, source)
- fragments = self.rx_css_split.split(compiled)
- at_rules = fragments.pop(0)
- if at_rules:
- # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
- self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
- while fragments:
- asset_id = fragments.pop(0)
- asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
- asset._content = fragments.pop(0)
- return '\n'.join(asset.minify() for asset in self.stylesheets)
- def compile_sass(self):
- self.preprocess_css()
- def compile_css(self, cmd, source):
- """Sanitizes @import rules, remove duplicates @import rules, then compile"""
- imports = []
- def sanitize(matchobj):
- ref = matchobj.group(2)
- line = '@import "%s"%s' % (ref, matchobj.group(3))
- if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
- imports.append(line)
- return line
- msg = "Local import '%s' is forbidden for security reasons." % ref
- _logger.warning(msg)
- self.css_errors.append(msg)
- return ''
- source = re.sub(self.rx_preprocess_imports, sanitize, source)
- try:
- compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- except Exception:
- msg = "Could not execute command %r" % cmd[0]
- _logger.error(msg)
- self.css_errors.append(msg)
- return ''
- result = compiler.communicate(input=source.encode('utf-8'))
- if compiler.returncode:
- error = self.get_preprocessor_error(''.join(result), source=source)
- _logger.warning(error)
- self.css_errors.append(error)
- return ''
- compiled = result[0].strip().decode('utf8')
- return compiled
- def get_preprocessor_error(self, stderr, source=None):
- """Improve and remove sensitive information from sass/less compilator error messages"""
- error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
- if 'Cannot load compass' in error:
- error += "Maybe you should install the compass gem using this extra argument:\n\n" \
- " $ sudo gem install compass --pre\n"
- error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
- for asset in self.stylesheets:
- if isinstance(asset, (SassAsset, LessStylesheetAsset)):
- error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
- return error
- class StylesheetAsset(openerp.addons.base.ir.ir_qweb.StylesheetAsset):
- @property
- def content(self):
- if self._content is None:
- self._content = self.inline or self._fetch_content()
- return self._content
- def _fetch_content(self):
- try:
- content = openerp.addons.base.ir.ir_qweb.WebAsset._fetch_content(self)
- web_dir = os.path.dirname(self.url)
- if self.rx_import:
- content = self.rx_import.sub(
- r"""@import \1%s/""" % (web_dir,),
- content,
- )
- if self.rx_url:
- content = self.rx_url.sub(
- r"url(\1%s/" % (web_dir,),
- content,
- )
- if self.rx_charset:
- # remove charset declarations, we only support utf-8
- content = self.rx_charset.sub('', content)
- return content
- except AssetError, e:
- self.bundle.css_errors.append(e.message)
- return ''
- class SassAsset(StylesheetAsset, openerp.addons.base.ir.ir_qweb.SassAsset):
- def to_html(self):
- if self.url:
- ira = self.registry['ir.attachment']
- url = self.html_url % self.url
- domain = [('type', '=', 'binary'), ('url', '=', url)]
- ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
- datas = self.content.encode('utf8').encode('base64')
- if ira_id:
- # TODO: update only if needed
- ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
- else:
- ira.create(self.cr, openerp.SUPERUSER_ID, dict(
- datas=datas,
- mimetype='text/css',
- type='binary',
- name=url,
- url=url,
- ), context=self.context)
- return super(SassAsset, self).to_html()
- def get_command(self):
- defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
- sass = which('sass', path=os.pathsep.join(defpath))
- return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
- '-r', 'bootstrap-sass']
- class LessStylesheetAsset(StylesheetAsset):
- html_url = '%s.css'
- rx_import = None
- def minify(self):
- return self.with_header()
- def to_html(self):
- if self.url:
- ira = self.registry['ir.attachment']
- url = self.html_url % self.url
- domain = [('type', '=', 'binary'), ('url', '=', url)]
- ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
- datas = self.content.encode('utf8').encode('base64')
- if ira_id:
- # TODO: update only if needed
- ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
- else:
- ira.create(self.cr, openerp.SUPERUSER_ID, dict(
- datas=datas,
- mimetype='text/css',
- type='binary',
- name=url,
- url=url,
- ), context=self.context)
- return super(LessStylesheetAsset, self).to_html()
- def get_source(self):
- content = self.inline or self._fetch_content()
- return "/*! %s */\n%s" % (self.id, content)
- def get_command(self):
- defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
- if os.name == 'nt':
- lessc = which('lessc.cmd', path=os.pathsep.join(defpath))
- else:
- lessc = which('lessc', path=os.pathsep.join(defpath))
- webpath = openerp.http.addons_manifest['web']['addons_path']
- lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
- return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
|