Files
odoo_source/addons/website/models/assets.py
T
Guillaume-gdi 3e8f9ccbc1 [FIX] website: make font import consistent
[This other commit] introduced a method to serve Google fonts from the
local server. Then it has been back-ported to previous versions with
[this commit].
Unfortunately, the font was not identical when the user chose to load
the font from Google servers versus from the local server. This
discrepancy was due to a missing parameter when downloading the font
file to serve it from the local server. This commit fixes the issue by
adding the missing parameter.

Steps to reproduce the issue fixed by this commit:
- Drop a text block onto a website page.
- Make the text bold.
- Go to the theme tab.
- Change the font to https://fonts.google.com/specimen/Poppins

=> The text style changes depending on whether you checked the "Serve
font from Google servers" option or not.

[This other commit]: https://github.com/odoo/odoo/commit/b06ce21eba6388ce34bbffffadcb489f0e8557dd
[this commit]: https://github.com/odoo/odoo/commit/04ab4e255b7fef1608ee2c70a3a005f3064bc4f3

opw-3775683

closes odoo/odoo#157800

X-original-commit: be2d27e69d7928a46d49a7ef0a3c4b93932a36b8
Signed-off-by: Romain Derie (rde) <rde@odoo.com>
Signed-off-by: Guillaume Dieleman (gdi) <gdi@odoo.com>
2024-03-15 10:43:56 +00:00

196 lines
9.2 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import re
import requests
from werkzeug.urls import url_parse
from odoo import api, models
class Assets(models.AbstractModel):
_inherit = 'web_editor.assets'
@api.model
def make_scss_customization(self, url, values):
"""
Makes a scss customization of the given file. That file must
contain a scss map including a line comment containing the word 'hook',
to indicate the location where to write the new key,value pairs.
Params:
url (str):
the URL of the scss file to customize (supposed to be a variable
file which will appear in the assets_frontend bundle)
values (dict):
key,value mapping to integrate in the file's map (containing the
word hook). If a key is already in the file's map, its value is
overridden.
"""
IrAttachment = self.env['ir.attachment']
if 'color-palettes-name' in values:
self.reset_asset('/website/static/src/scss/options/colors/user_color_palette.scss', 'web.assets_frontend')
self.reset_asset('/website/static/src/scss/options/colors/user_gray_color_palette.scss', 'web.assets_frontend')
# Do not reset all theme colors for compatibility (not removing alpha -> epsilon colors)
self.make_scss_customization('/website/static/src/scss/options/colors/user_theme_color_palette.scss', {
'success': 'null',
'info': 'null',
'warning': 'null',
'danger': 'null',
})
# Also reset gradients which are in the "website" values palette
self.make_scss_customization('/website/static/src/scss/options/user_values.scss', {
'menu-gradient': 'null',
'menu-secondary-gradient': 'null',
'footer-gradient': 'null',
'copyright-gradient': 'null',
})
delete_attachment_id = values.pop('delete-font-attachment-id', None)
if delete_attachment_id:
delete_attachment_id = int(delete_attachment_id)
IrAttachment.search([
'|', ('id', '=', delete_attachment_id),
('original_id', '=', delete_attachment_id),
('name', 'like', 'google-font'),
]).unlink()
google_local_fonts = values.get('google-local-fonts')
if google_local_fonts and google_local_fonts != 'null':
# "('font_x': 45, 'font_y': '')" -> {'font_x': '45', 'font_y': ''}
google_local_fonts = dict(re.findall(r"'([^']+)': '?(\d*)", google_local_fonts))
# Google is serving different font format (woff, woff2, ttf, eot..)
# based on the user agent. We need to get the woff2 as this is
# supported by all the browers we support.
headers_woff2 = {
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36',
}
for font_name in google_local_fonts:
if google_local_fonts[font_name]:
google_local_fonts[font_name] = int(google_local_fonts[font_name])
else:
font_family_attachments = IrAttachment
font_content = requests.get(
f'https://fonts.googleapis.com/css?family={font_name}:300,300i,400,400i,700,700i&display=swap',
timeout=5, headers=headers_woff2,
).content.decode()
def fetch_google_font(src):
statement = src.group()
url, font_format = re.match(r'src: url\(([^\)]+)\) (.+)', statement).groups()
req = requests.get(url, timeout=5, headers=headers_woff2)
# https://fonts.gstatic.com/s/modak/v18/EJRYQgs1XtIEskMB-hRp7w.woff2
# -> s-modak-v18-EJRYQgs1XtIEskMB-hRp7w.woff2
name = url_parse(url).path.lstrip('/').replace('/', '-')
attachment = IrAttachment.create({
'name': f'google-font-{name}',
'type': 'binary',
'datas': base64.b64encode(req.content),
'public': True,
})
nonlocal font_family_attachments
font_family_attachments += attachment
return 'src: url(/web/content/%s/%s) %s' % (
attachment.id,
name,
font_format,
)
font_content = re.sub(r'src: url\(.+\)', fetch_google_font, font_content)
attach_font = IrAttachment.create({
'name': f'{font_name} (google-font)',
'type': 'binary',
'datas': base64.encodebytes(font_content.encode()),
'mimetype': 'text/css',
'public': True,
})
google_local_fonts[font_name] = attach_font.id
# That field is meant to keep track of the original
# image attachment when an image is being modified (by the
# website builder for instance). It makes sense to use it
# here to link font family attachment to the main font
# attachment. It will ease the unlink later.
font_family_attachments.original_id = attach_font.id
# {'font_x': 45, 'font_y': 55} -> "('font_x': 45, 'font_y': 55)"
values['google-local-fonts'] = str(google_local_fonts).replace('{', '(').replace('}', ')')
custom_url = self._make_custom_asset_url(url, 'web.assets_frontend')
updatedFileContent = self._get_content_from_url(custom_url) or self._get_content_from_url(url)
updatedFileContent = updatedFileContent.decode('utf-8')
for name, value in values.items():
# Protect variable names so they cannot be computed as numbers
# on SCSS compilation (e.g. var(--700) => var(700)).
if isinstance(value, str):
value = re.sub(
r"var\(--([0-9]+)\)",
lambda matchobj: "var(--#{" + matchobj.group(1) + "})",
value)
pattern = "'%s': %%s,\n" % name
regex = re.compile(pattern % ".+")
replacement = pattern % value
if regex.search(updatedFileContent):
updatedFileContent = re.sub(regex, replacement, updatedFileContent)
else:
updatedFileContent = re.sub(r'( *)(.*hook.*)', r'\1%s\1\2' % replacement, updatedFileContent)
self.save_asset(url, 'web.assets_frontend', updatedFileContent, 'scss')
@api.model
def _get_custom_attachment(self, custom_url, op='='):
"""
See web_editor.Assets._get_custom_attachment
Extend to only return the attachments related to the current website.
"""
if self.env.user.has_group('website.group_website_designer'):
self = self.sudo()
website = self.env['website'].get_current_website()
res = super()._get_custom_attachment(custom_url, op=op)
# See _save_asset_attachment_hook -> it is guaranteed that the
# attachment we are looking for has a website_id. When we serve an
# attachment we normally serve the ones which have the right website_id
# or no website_id at all (which means "available to all websites", of
# course if they are marked "public"). But this does not apply in this
# case of customized asset files.
return res.with_context(website_id=website.id).filtered(lambda x: x.website_id == website)
@api.model
def _get_custom_asset(self, custom_url):
"""
See web_editor.Assets._get_custom_asset
Extend to only return the views related to the current website.
"""
if self.env.user.has_group('website.group_website_designer'):
# TODO: Remove me in master, see commit message, ACL added right to
# unlink to designer but not working without -u in stable
self = self.sudo()
website = self.env['website'].get_current_website()
res = super()._get_custom_asset(custom_url)
return res.with_context(website_id=website.id).filter_duplicate()
@api.model
def _add_website_id(self, values):
website = self.env['website'].get_current_website()
values['website_id'] = website.id
return values
@api.model
def _save_asset_attachment_hook(self):
"""
See web_editor.Assets._save_asset_attachment_hook
Extend to add website ID at ir.attachment creation.
"""
return self._add_website_id(super()._save_asset_attachment_hook())
@api.model
def _save_asset_hook(self):
"""
See web_editor.Assets._save_asset_hook
Extend to add website ID at ir.asset creation.
"""
return self._add_website_id(super()._save_asset_hook())