From f159ba8f31126e04e01aeb9ac8e53d9ce2689fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Fri, 26 Apr 2019 10:55:39 +0000 Subject: [PATCH] [IMP] tools,website: correctly handle favicon and .ico files Previously the image that we uploaded as a favicon of the website was not resized, so it could be extremely excessive in size and resolution, and it also was not guaranteed to be a square. Now we always resize it appropriately and save it as an ICO file. The default favicon is now applied to all websites instead of only the first. task-1958000 PR: #31811 --- addons/website/data/website_data.xml | 1 - addons/website/models/website.py | 19 ++++++++++- addons/website/tests/__init__.py | 1 + addons/website/tests/test_website_favicon.py | 36 ++++++++++++++++++++ odoo/tools/image.py | 7 ++-- odoo/tools/mimetypes.py | 1 + 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 addons/website/tests/test_website_favicon.py diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index 9701610dde3..737ff819de8 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -156,7 +156,6 @@ - diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 0261b1fc616..4f55ed27298 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 import inspect import logging import hashlib @@ -14,6 +15,7 @@ from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype from odoo.addons.website.models.ir_http import sitemap_qs2dom from odoo.addons.portal.controllers.portal import pager from odoo.http import request +from odoo.modules.module import get_resource_path from odoo.osv.expression import FALSE_DOMAIN from odoo.tools.translate import _ @@ -100,7 +102,13 @@ class Website(models.Model): partner_id = fields.Many2one(related='user_id.partner_id', relation='res.partner', string='Public Partner', readonly=False) menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu') homepage_id = fields.Many2one('website.page', string='Homepage') - favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.") + + def _default_favicon(self): + img_path = get_resource_path('web', 'static/src/img/favicon.ico') + with tools.file_open(img_path, 'rb') as f: + return base64.b64encode(f.read()) + + favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.", default=_default_favicon) theme_id = fields.Many2one('ir.module.module', help='Installed theme') specific_user_account = fields.Boolean('Specific User Account', help='If True, new accounts will be associated to the current website') @@ -122,6 +130,8 @@ class Website(models.Model): @api.model def create(self, vals): + self._handle_favicon(vals) + if 'user_id' not in vals: company = self.env['res.company'].browse(vals.get('company_id')) vals['user_id'] = company._get_public_user().id if company else self.env.ref('base.public_user').id @@ -138,6 +148,8 @@ class Website(models.Model): @api.multi def write(self, values): + self._handle_favicon(values) + self._get_languages.clear_cache(self) if 'company_id' in values and 'user_id' not in values: company = self.env['res.company'].browse(values['company_id']) @@ -149,6 +161,11 @@ class Website(models.Model): self.env['ir.qweb'].clear_caches() return result + @api.model + def _handle_favicon(self, vals): + if 'favicon' in vals: + vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO') + # ---------------------------------------------------------- # Page Management # ---------------------------------------------------------- diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 85ecad1cba8..f1f4a459cee 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -9,4 +9,5 @@ from . import test_ui from . import test_views from . import test_menu from . import test_page +from . import test_website_favicon from . import test_website_reset_password diff --git a/addons/website/tests/test_website_favicon.py b/addons/website/tests/test_website_favicon.py new file mode 100644 index 00000000000..3346ccd459f --- /dev/null +++ b/addons/website/tests/test_website_favicon.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from PIL import Image + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase +from odoo.tools import base64_to_image, image_to_base64 + + +@tagged('post_install', '-at_install') +class TestWebsiteResetPassword(TransactionCase): + + def test_01_website_favicon(self): + """The goal of this test is to make sure the favicon is correctly + handled on the website.""" + + # Test setting an Ico file directly, done through create + Website = self.env['website'] + + website = Website.create({ + 'name': 'Test Website', + 'favicon': Website._default_favicon(), + }) + + image = base64_to_image(website.favicon) + self.assertEqual(image.format, 'ICO') + + # Test setting a JPEG file that is too big, done through write + bg_color = (135, 90, 123) + image = Image.new('RGB', (1920, 1080), color=bg_color) + website.favicon = image_to_base64(image, 'JPEG') + image = base64_to_image(website.favicon) + self.assertEqual(image.format, 'ICO') + self.assertEqual(image.size, (256, 256)) + self.assertEqual(image.getpixel((0, 0)), bg_color) diff --git a/odoo/tools/image.py b/odoo/tools/image.py index 5ae96359181..c45cfd77070 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -4,6 +4,9 @@ import base64 import io from PIL import Image +# We can preload Ico too because it is considered safe +from PIL import IcoImagePlugin + from random import randrange from odoo.tools.translate import _ @@ -71,7 +74,7 @@ def image_process(base64_source, size=(0, 0), verify_resolution=False, quality=8 :param colorize: replace the trasparent background by a random color :type colorize: bool - :param output_format: the output format. Can be PNG, JPEG or GIF. + :param output_format: the output format. Can be PNG, JPEG, GIF, or ICO. Default to the format of the original image. BMP is converted to PNG, other formats are converted to JPEG. :type output_format: string @@ -99,7 +102,7 @@ def image_process(base64_source, size=(0, 0), verify_resolution=False, quality=8 output_format = (output_format or image.format).upper() if output_format == 'BMP': output_format = 'PNG' - elif output_format not in ['PNG', 'JPEG', 'GIF']: + elif output_format not in ['PNG', 'JPEG', 'GIF', 'ICO']: output_format = 'JPEG' opt = {'format': output_format} diff --git a/odoo/tools/mimetypes.py b/odoo/tools/mimetypes.py index 4350e8c71a5..79bced25c5d 100644 --- a/odoo/tools/mimetypes.py +++ b/odoo/tools/mimetypes.py @@ -125,6 +125,7 @@ _mime_mappings = ( _Entry('image/svg+xml', [b'<'], [ _check_svg, ]), + _Entry('image/x-icon', [b'\x00\x00\x01\x00'], []), # 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