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