summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/web/controllers/main.py8
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js9
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js9
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js1
-rw-r--r--addons/website_slides/static/src/js/slides_upload.js1
-rw-r--r--odoo/addons/base/models/ir_attachment.py3
-rw-r--r--odoo/addons/base/models/res_users.py2
-rw-r--r--odoo/addons/base/tests/test_mimetypes.py14
-rw-r--r--odoo/addons/test_new_api/ir.model.access.csv1
-rw-r--r--odoo/addons/test_new_api/models.py9
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py56
-rw-r--r--odoo/fields.py14
-rw-r--r--odoo/tools/image.py5
-rw-r--r--odoo/tools/mimetypes.py10
14 files changed, 134 insertions, 8 deletions
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index c2027984c42..ed2e9f13062 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -37,6 +37,7 @@ import odoo.modules.registry
from odoo.api import call_kw, Environment
from odoo.modules import get_resource_path
from odoo.tools import crop_image, topological_sort, html_escape, pycompat
+from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.translate import _
from odoo.tools.misc import str2bool, xlwt, file_open
from odoo.tools.safe_eval import safe_eval
@@ -1218,8 +1219,11 @@ class Binary(http.Controller):
if row and row[0]:
image_base64 = base64.b64decode(row[0])
image_data = io.BytesIO(image_base64)
- imgext = '.' + (imghdr.what(None, h=image_base64) or 'png')
- response = http.send_file(image_data, filename=imgname + imgext, mtime=row[1])
+ 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:
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 0c28f0f4b61..12b7b8f302c 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1471,12 +1471,19 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
},
}),
supportedFieldTypes: ['binary'],
+ file_type_magic_word: {
+ '/': 'jpg',
+ 'R': 'gif',
+ 'i': 'png',
+ 'P': 'svg+xml',
+ },
_render: function () {
var self = this;
var url = this.placeholder;
if (this.value) {
if (!utils.is_bin_size(this.value)) {
- url = 'data:image/png;base64,' + this.value;
+ // Use magic-word technique for detecting image type
+ url = 'data:image/' + (this.file_type_magic_word[this.value[0]] || 'png') + ';base64,' + this.value;
} else {
url = session.url('/web/image', {
model: this.model,
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 9be296e3e31..7013464ea87 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -166,6 +166,12 @@ var KanbanRecord = Widget.extend({
var colorID = this._getColorID(variable);
return KANBAN_RECORD_COLORS[colorID];
},
+ file_type_magic_word: {
+ '/': 'jpg',
+ 'R': 'gif',
+ 'i': 'png',
+ 'P': 'svg+xml',
+ },
/**
* @private
* @param {string} model the name of the model
@@ -179,7 +185,8 @@ var KanbanRecord = Widget.extend({
options = options || {};
var url;
if (this.record[field] && this.record[field].value && !utils.is_bin_size(this.record[field].value)) {
- url = 'data:image/png;base64,' + this.record[field].value;
+ // Use magic-word technique for detecting image type
+ url = 'data:image/' + this.file_type_magic_word[this.record[field].value[0]] + ';base64,' + this.record[field].value;
} else if (this.record[field] && ! this.record[field].value) {
url = "/web/static/src/img/placeholder.png";
} else {
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index dd0982bf3be..2c06743b361 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -363,6 +363,7 @@ var ImageWidget = MediaWidget.extend({
if (!noRender) {
this.$('input.url').val('').trigger('input').trigger('change');
}
+ // TODO: Expand this for adding SVG
var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]);
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js
index 8aa18dc7381..aa638882930 100644
--- a/addons/website_slides/static/src/js/slides_upload.js
+++ b/addons/website_slides/static/src/js/slides_upload.js
@@ -298,6 +298,7 @@ var SlideDialog = Widget.extend({
});
return res;
},
+ // TODO: Remove this part, as now SVG support in image resize tools is included
//Python PIL does not support SVG, so converting SVG to PNG
svg_to_png: function () {
var img = this.$el.find("img#slide-image")[0];
diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py
index 94b88536932..3cca0bb85a7 100644
--- a/odoo/addons/base/models/ir_attachment.py
+++ b/odoo/addons/base/models/ir_attachment.py
@@ -254,7 +254,8 @@ class IrAttachment(models.Model):
def _check_contents(self, values):
mimetype = values['mimetype'] = self._compute_mimetype(values)
xml_like = 'ht' in mimetype or 'xml' in mimetype # hta, html, xhtml, etc.
- force_text = (xml_like and (not self.env.user._is_admin() or
+ user = self.env.context.get('binary_field_real_user', self.env.user)
+ force_text = (xml_like and (not user._is_system() or
self.env.context.get('attachments_mime_plainxml')))
if force_text:
values['mimetype'] = 'text/plain'
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 247fb367334..850c21d6b3f 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -449,7 +449,7 @@ class Users(models.Model):
if values['company_id'] not in self.env.user.company_ids.ids:
del values['company_id']
# safe fields only, so we write as super-user to bypass access rights
- self = self.sudo()
+ self = self.sudo().with_context(binary_field_real_user=self.env.user)
res = super(Users, self).write(values)
if 'company_id' in values:
diff --git a/odoo/addons/base/tests/test_mimetypes.py b/odoo/addons/base/tests/test_mimetypes.py
index 2f2f1433c3e..3ca506cb744 100644
--- a/odoo/addons/base/tests/test_mimetypes.py
+++ b/odoo/addons/base/tests/test_mimetypes.py
@@ -16,6 +16,11 @@ AAAAAAAAAAAAA/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/AX//xAAUEQE
AA/9oACAECAQE/AX//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAY/An//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBA
AE/IX//2gAMAwEAAgADAAAAEB//xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDAQE/EH//xAAUEQEAAAAAAAAAAAAAAAAAAAAA
/9oACAECAQE/EH//xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oACAEBAAE/EH//2Q=="""
+SVG = b"""PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiL
+S8vVzNDLy9EVEQgU1ZHIDIwMDAxMTAyLy9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAwL0NSLVNWRy0yMDAwMTEwMi9E
+VEQvc3ZnLTIwMDAxMTAyLmR0ZCI+Cgo8c3ZnIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPgogIDxnIHRyYW5zZm9ybT0idHJ
+hbnNsYXRlKDUwLDUwKSI+CiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjUwIiBzdHlsZT0iZmlsbD
+pyZWQ7IiAvPgogIDwvZz4KCjwvc3ZnPgo="""
@tagged('standard', 'at_install')
@@ -57,6 +62,15 @@ class test_guess_mimetype(unittest.TestCase):
mimetype = guess_mimetype(content, default='test')
self.assertEqual(mimetype, 'image/gif')
+ def test_mimetype_svg(self):
+ content = base64.b64decode(SVG)
+ mimetype = guess_mimetype(content, default='test')
+ self.assertTrue(mimetype.startswith('image/svg'))
+ # Tests that whitespace padded SVG are not detected as SVG
+ mimetype = guess_mimetype(b" " + content, default='test')
+ self.assertNotIn("svg", mimetype)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv
index 46a658c818d..c825fb51c59 100644
--- a/odoo/addons/test_new_api/ir.model.access.csv
+++ b/odoo/addons/test_new_api/ir.model.access.csv
@@ -21,3 +21,4 @@ access_test_new_api_compute_protected,access_test_new_api_compute_protected,mode
access_test_new_api_multi_compute_inverse,access_test_new_api_multi_compute_inverse,model_test_new_api_multi_compute_inverse,,1,1,1,1
access_test_new_api_recursive,access_test_new_api_recursive,model_test_new_api_recursive,,1,1,1,1
access_test_new_api_cascade,access_test_new_api_cascade,model_test_new_api_cascade,,1,1,1,1
+access_test_new_api_binary_svg,access_test_new_api_binary_svg,model_test_new_api_binary_svg,,1,1,1,1
diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py
index bc0a3e45876..043ee8a0b7d 100644
--- a/odoo/addons/test_new_api/models.py
+++ b/odoo/addons/test_new_api/models.py
@@ -459,3 +459,12 @@ class ComputeCascade(models.Model):
def _compute_baz(self):
for record in self:
record.baz = "<%s>" % (record.bar or "")
+
+
+class BinarySvg(models.Model):
+ _name = 'test_new_api.binary_svg'
+ _description = 'Test SVG upload'
+
+ name = fields.Char(required=True)
+ image_attachment = fields.Binary(attachment=True)
+ image_wo_attachment = fields.Binary(attachment=False)
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index 8284f4652c6..caa4eedf4bf 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -998,6 +998,62 @@ class TestFields(common.TransactionCase):
self.assertEqual(count(message), 1)
self.assertEqual(count(message1), 0)
+ def test_90_binary_svg(self):
+ from odoo.addons.base.tests.test_mimetypes import SVG
+ # This should work without problems
+ self.env['test_new_api.binary_svg'].create({
+ 'name': 'Test without attachment',
+ 'image_wo_attachment': SVG,
+ })
+ # And this gives error
+ with self.assertRaises(UserError):
+ self.env['test_new_api.binary_svg'].sudo(
+ self.env.ref('base.user_demo'),
+ ).create({
+ 'name': 'Test without attachment',
+ 'image_wo_attachment': SVG,
+ })
+
+ def test_91_binary_svg_attachment(self):
+ from odoo.addons.base.tests.test_mimetypes import SVG
+ # This doesn't neuter SVG with admin
+ record = self.env['test_new_api.binary_svg'].create({
+ 'name': 'Test without attachment',
+ 'image_attachment': SVG,
+ })
+ attachment = self.env['ir.attachment'].search([
+ ('res_model', '=', record._name),
+ ('res_field', '=', 'image_attachment'),
+ ('res_id', '=', record.id),
+ ])
+ self.assertEqual(attachment.mimetype, 'image/svg+xml')
+ # ...but this should be neutered with demo user
+ record = self.env['test_new_api.binary_svg'].sudo(
+ self.env.ref('base.user_demo'),
+ ).create({
+ 'name': 'Test without attachment',
+ 'image_attachment': SVG,
+ })
+ attachment = self.env['ir.attachment'].search([
+ ('res_model', '=', record._name),
+ ('res_field', '=', 'image_attachment'),
+ ('res_id', '=', record.id),
+ ])
+ self.assertEqual(attachment.mimetype, 'text/plain')
+
+ def test_92_binary_self_avatar_svg(self):
+ from odoo.addons.base.tests.test_mimetypes import SVG
+ demo_user = self.env.ref('base.user_demo')
+ # User demo changes his own avatar
+ demo_user.sudo(demo_user).image = SVG
+ # The SVG file should have been neutered
+ attachment = self.env['ir.attachment'].search([
+ ('res_model', '=', demo_user.partner_id._name),
+ ('res_field', '=', 'image'),
+ ('res_id', '=', demo_user.partner_id.id),
+ ])
+ self.assertEqual(attachment.mimetype, 'text/plain')
+
class TestX2many(common.TransactionCase):
def test_search_many2many(self):
diff --git a/odoo/fields.py b/odoo/fields.py
index 8d2f939fd88..a146a1bfa07 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -10,6 +10,7 @@ from functools import partial
from operator import attrgetter
import itertools
import logging
+import base64
import pytz
@@ -27,6 +28,7 @@ from .tools import float_repr, float_round, frozendict, html_sanitize, human_siz
from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
from .tools.translate import html_translate, _
+from .tools.mimetypes import guess_mimetype
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
@@ -1752,6 +1754,14 @@ class Binary(Field):
# on purpose - non base64 data must be passed as a 8bit byte strings.
if not value:
return None
+ # Detect if the binary content is an SVG for restricting its upload
+ # only to system users.
+ if value[:1] == b'P': # Fast detection of first 6 bits of '<' (0x3C)
+ decoded_value = base64.b64decode(value)
+ # Full mimetype detection
+ if (guess_mimetype(decoded_value).startswith('image/svg') and
+ not record.env.user._is_system()):
+ raise UserError(_("Only admins can upload SVG files."))
if isinstance(value, bytes):
return psycopg2.Binary(value)
try:
@@ -1793,7 +1803,9 @@ class Binary(Field):
# create the attachments that store the values
env = record_values[0][0].env
with env.norecompute():
- env['ir.attachment'].sudo().create([{
+ env['ir.attachment'].sudo().with_context(
+ binary_field_real_user=env.user,
+ ).create([{
'name': self.name,
'res_model': self.model_name,
'res_field': self.name,
diff --git a/odoo/tools/image.py b/odoo/tools/image.py
index 887ba823b96..24aa56ced17 100644
--- a/odoo/tools/image.py
+++ b/odoo/tools/image.py
@@ -51,7 +51,10 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
"""
if not base64_source:
return False
- if size == (None, None):
+ # Return unmodified content if no resize or we etect first 6 bits of '<'
+ # (0x3C) for SVG documents - This will bypass XML files as well, but it's
+ # harmless for these purposes
+ if size == (None, None) or base64_source[:1] == b'P':
return base64_source
image_stream = io.BytesIO(codecs.decode(base64_source, encoding))
image = Image.open(image_stream)
diff --git a/odoo/tools/mimetypes.py b/odoo/tools/mimetypes.py
index 59421cf8bb6..4350e8c71a5 100644
--- a/odoo/tools/mimetypes.py
+++ b/odoo/tools/mimetypes.py
@@ -102,6 +102,13 @@ def _check_olecf(data):
return 'application/vnd.ms-powerpoint'
return False
+
+def _check_svg(data):
+ """This simply checks the existence of the opening and ending SVG tags"""
+ if b'<svg' in data and b'/svg>' in data:
+ return 'image/svg+xml'
+
+
# for "master" formats with many subformats, discriminants is a list of
# functions, tried in order and the first non-falsy value returned is the
# selected mime type. If all functions return falsy values, the master
@@ -115,6 +122,9 @@ _mime_mappings = (
_Entry('image/png', [b'\x89PNG\r\n\x1A\n'], []),
_Entry('image/gif', [b'GIF87a', b'GIF89a'], []),
_Entry('image/bmp', [b'BM'], []),
+ _Entry('image/svg+xml', [b'<'], [
+ _check_svg,
+ ]),
# OLECF files in general (Word, Excel, PPT, default to word because why not?)
_Entry('application/msword', [b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', b'\x0D\x44\x4F\x43'], [
_check_olecf