Before this commit, the only possibility when adding a google font was to use google servers to serve the font. This was not ideal as some people really want to serve it themselves without the need of their visitors to reach google servers. That's especially true since recently where it seems like German clients are receiving letters about that to tell them it's illegal and this should be changed, as it wouldn't respect the GDPR. Somehow, it seems related to the fact that google knows you visited a website by just downloading the font, because they very well know with just your IP who you are exactly. It's yet unsure if that issue is well-founded or not, but since German courts seem to be sanctioning people about this, there is no reason to not at least provide a workaround. What is sure is that it makes a lot of noise and more and more people seem to be impacted by this as many opw are getting opened, as well as github messages. Whether it is well-founded or not is thus not really our problem anymore, we should just provide a way for our users to protect themselves against this "German law problem" (or at least think they are protecting, if Odoo thinks that's a non issue or the German court is wrong or ambiguous). Note that a cookies banner to inform users would not be enough for that "problem", as the user would already have accessed your website and thus the related problematic fonts. Another solution which is not something we want (at all) would be to serve local system fonts while the user did not consent about google fonts, or having a blocking screen page telling people visiting the website will fetch google fonts. Obviously those 2 possibilities are a no go as it leads to terrible UX. Finally, note that: - in Odoo 16, the default fonts will be the system fonts, meaning there won't be any call to google by default, regardless of this pr - there is a work in progress to improve the current cookies bar to differentiate essential and non essential cookies and to allow user to accept only one or both (task-2800976). Useful links: - https://github.com/odoo/odoo/issues/83638#issuecomment-1054470699 ODO detailed point of view about this - https://rewis.io/urteile/urteil/lhm-20-01-2022-3-o-1749320/ The German law about this Closes #83638 task-2756486 opw-2970167 opw-2960466 opw-2960555 opw-2952427 opw-2800976 opw-2748647 (possibly many more) Courtesy of @bso-odoo for the regex part which was inspired by another of his google font fix attempt closes odoo/odoo#101826 X-original-commit: b06ce21eba6388ce34bbffffadcb489f0e8557dd Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Signed-off-by: Romain Derie (rde) <rde@odoo.com>
174 lines
8.1 KiB
Python
174 lines
8.1 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',
|
|
'header-boxed-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}&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():
|
|
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)
|
|
return res.with_context(website_id=website.id).filtered(lambda x: not x.website_id or 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 _save_asset_hook(self):
|
|
"""
|
|
See web_editor.Assets._save_asset_hook
|
|
Extend to add website ID at attachment creation.
|
|
"""
|
|
res = super()._save_asset_hook()
|
|
|
|
website = self.env['website'].get_current_website()
|
|
if website:
|
|
res['website_id'] = website.id
|
|
return res
|