# Part of Odoo. See LICENSE file for full copyright and licensing details. import base64 import functools import io import json import logging import os import unicodedata import odoo import odoo.modules.registry from odoo import http from odoo.exceptions import AccessError from odoo.http import request from odoo.modules import get_resource_path from odoo.tools import pycompat from odoo.tools.mimetypes import guess_mimetype from odoo.tools.misc import file_open, file_path from odoo.tools.translate import _ _logger = logging.getLogger(__name__) def clean(name): return name.replace('\x3c', '') class Binary(http.Controller): @http.route(['/web/content', '/web/content/', '/web/content//', '/web/content/', '/web/content//', '/web/content///', '/web/content////'], type='http', auth="public") def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas', filename=None, filename_field='name', unique=None, mimetype=None, download=None, data=None, token=None, access_token=None, **kw): return request.env['ir.http']._get_content_common(xmlid=xmlid, model=model, res_id=id, field=field, unique=unique, filename=filename, filename_field=filename_field, download=download, mimetype=mimetype, access_token=access_token, token=token) @http.route(['/web/assets/debug/', '/web/assets/debug//', '/web/assets//', '/web/assets/-/', '/web/assets/-//'], type='http', auth="public") def content_assets(self, id=None, filename=None, unique=None, extra=None, **kw): id = id or request.env['ir.attachment'].sudo().search_read( [('url', '=like', f'/web/assets/%/{extra}/{filename}' if extra else f'/web/assets/%/{filename}')], fields=['id'], limit=1)[0]['id'] return request.env['ir.http']._get_content_common(xmlid=None, model='ir.attachment', res_id=id, field='datas', unique=unique, filename=filename, filename_field='name', download=None, mimetype=None, access_token=None, token=None) @http.route(['/web/image', '/web/image/', '/web/image//', '/web/image//x', '/web/image//x/', '/web/image///', '/web/image////', '/web/image////x', '/web/image////x/', '/web/image/', '/web/image//', '/web/image//x', '/web/image//x/', '/web/image/-', '/web/image/-/', '/web/image/-/x', '/web/image/-/x/'], type='http', auth="public") def content_image(self, xmlid=None, model='ir.attachment', id=None, field='raw', filename_field='name', unique=None, filename=None, mimetype=None, download=None, width=0, height=0, crop=False, access_token=None, **kwargs): # other kwargs are ignored on purpose return request.env['ir.http']._content_image(xmlid=xmlid, model=model, res_id=id, field=field, filename_field=filename_field, unique=unique, filename=filename, mimetype=mimetype, download=download, width=width, height=height, crop=crop, quality=int(kwargs.get('quality', 0)), access_token=access_token) # backward compatibility @http.route(['/web/binary/image'], type='http', auth="public") def content_image_backward_compatibility(self, model, id, field, resize=None, **kw): width = None height = None if resize: width, height = resize.split(",") return request.env['ir.http']._content_image(model=model, res_id=id, field=field, width=width, height=height) @http.route('/web/binary/upload', type='http', auth="user") def upload(self, ufile, callback=None): # TODO: might be useful to have a configuration flag for max-length file uploads out = """""" try: data = ufile.read() args = [len(data), ufile.filename, ufile.content_type, pycompat.to_text(base64.b64encode(data))] except Exception as e: args = [False, str(e)] return out % (json.dumps(clean(callback)), json.dumps(args)) if callback else json.dumps(args) @http.route('/web/binary/upload_attachment', type='http', auth="user") def upload_attachment(self, model, id, ufile, callback=None): files = request.httprequest.files.getlist('ufile') Model = request.env['ir.attachment'] out = """""" args = [] for ufile in files: filename = ufile.filename if request.httprequest.user_agent.browser == 'safari': # Safari sends NFD UTF-8 (where é is composed by 'e' and [accent]) # we need to send it the same stuff, otherwise it'll fail filename = unicodedata.normalize('NFD', ufile.filename) try: cids = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id)) allowed_company_ids = [int(cid) for cid in cids.split(',')] attachment = Model.with_context(allowed_company_ids=allowed_company_ids).create({ 'name': filename, 'datas': base64.encodebytes(ufile.read()), 'res_model': model, 'res_id': int(id) }) attachment._post_add_create() except AccessError: args.append({'error': _("You are not allowed to upload an attachment here.")}) except Exception: args.append({'error': _("Something horrible happened")}) _logger.exception("Fail to upload attachment %s", ufile.filename) else: args.append({ 'filename': clean(filename), 'mimetype': ufile.content_type, 'id': attachment.id, 'size': attachment.file_size }) return out % (json.dumps(clean(callback)), json.dumps(args)) if callback else json.dumps(args) @http.route([ '/web/binary/company_logo', '/logo', '/logo.png', ], type='http', auth="none", cors="*") def company_logo(self, dbname=None, **kw): imgname = 'logo' imgext = '.png' placeholder = functools.partial(get_resource_path, 'web', 'static', 'img') dbname = request.db uid = (request.session.uid if dbname else None) or odoo.SUPERUSER_ID if not dbname: response = http.send_file(placeholder(imgname + imgext)) else: try: # create an empty registry registry = odoo.modules.registry.Registry(dbname) with registry.cursor() as cr: company = int(kw['company']) if kw and kw.get('company') else False if company: cr.execute("""SELECT logo_web, write_date FROM res_company WHERE id = %s """, (company,)) else: cr.execute("""SELECT c.logo_web, c.write_date FROM res_users u LEFT JOIN res_company c ON c.id = u.company_id WHERE u.id = %s """, (uid,)) row = cr.fetchone() if row and row[0]: image_base64 = base64.b64decode(row[0]) image_data = io.BytesIO(image_base64) mimetype = guess_mimetype(image_base64, default='image/png') imgext = '.' + mimetype.split('/')[1] if imgext == '.svg+xml': imgext = '.svg' response = http.send_file(image_data, filename=imgname + imgext, mimetype=mimetype, mtime=row[1]) else: response = http.send_file(placeholder('nologo.png')) except Exception: response = http.send_file(placeholder(imgname + imgext)) return response @http.route(['/web/sign/get_fonts', '/web/sign/get_fonts/'], type='json', auth='public') def get_fonts(self, fontname=None): """This route will return a list of base64 encoded fonts. Those fonts will be proposed to the user when creating a signature using mode 'auto'. :return: base64 encoded fonts :rtype: list """ supported_exts = ('.ttf', '.otf', '.woff', '.woff2') fonts = [] fonts_directory = file_path(os.path.join('web', 'static', 'fonts', 'sign')) if fontname: font_path = os.path.join(fonts_directory, fontname) with file_open(font_path, 'rb', filter_ext=supported_exts) as font_file: font = base64.b64encode(font_file.read()) fonts.append(font) else: font_filenames = sorted([fn for fn in os.listdir(fonts_directory) if fn.endswith(supported_exts)]) for filename in font_filenames: font_file = file_open(os.path.join(fonts_directory, filename), 'rb', filter_ext=supported_exts) font = base64.b64encode(font_file.read()) fonts.append(font) return fonts