[IMP] website: add possibility to self host google font
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>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
# -*- 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
|
||||
|
||||
@@ -26,6 +30,7 @@ class Assets(models.AbstractModel):
|
||||
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')
|
||||
@@ -44,6 +49,76 @@ class Assets(models.AbstractModel):
|
||||
'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')
|
||||
|
||||
@@ -116,6 +116,9 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
const googleFontsProperty = weUtils.getCSSVariableValue('google-fonts', style);
|
||||
this.googleFonts = googleFontsProperty ? googleFontsProperty.split(/\s*,\s*/g) : [];
|
||||
this.googleFonts = this.googleFonts.map(font => font.substring(1, font.length - 1)); // Unquote
|
||||
const googleLocalFontsProperty = weUtils.getCSSVariableValue('google-local-fonts', style);
|
||||
this.googleLocalFonts = googleLocalFontsProperty ?
|
||||
googleLocalFontsProperty.slice(1, -1).split(/\s*,\s*/g) : [];
|
||||
|
||||
await this._super(...arguments);
|
||||
|
||||
@@ -136,14 +139,25 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
this.menuEl.appendChild(fontEl);
|
||||
});
|
||||
|
||||
if (this.googleLocalFonts.length) {
|
||||
const googleLocalFontsEls = fontEls.splice(-this.googleLocalFonts.length);
|
||||
googleLocalFontsEls.forEach((el, index) => {
|
||||
$(el).append(core.qweb.render('website.delete_google_font_btn', {
|
||||
index: index,
|
||||
local: true,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.googleFonts.length) {
|
||||
const googleFontsEls = fontEls.slice(-this.googleFonts.length);
|
||||
const googleFontsEls = fontEls.splice(-this.googleFonts.length);
|
||||
googleFontsEls.forEach((el, index) => {
|
||||
$(el).append(core.qweb.render('website.delete_google_font_btn', {
|
||||
index: index,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
$(this.menuEl).append($(core.qweb.render('website.add_google_font_btn', {
|
||||
variable: variable,
|
||||
})));
|
||||
@@ -217,10 +231,16 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
}
|
||||
|
||||
const font = m[1].replace(/\+/g, ' ');
|
||||
this.googleFonts.push(font);
|
||||
const googleFontServe = dialog.el.querySelector('#google_font_serve').checked;
|
||||
if (googleFontServe) {
|
||||
this.googleFonts.push(font);
|
||||
} else {
|
||||
this.googleLocalFonts.push(`'${font}': ''`);
|
||||
}
|
||||
this.trigger_up('google_fonts_custo_request', {
|
||||
values: {[variable]: `'${font}'`},
|
||||
googleFonts: this.googleFonts,
|
||||
googleLocalFonts: this.googleLocalFonts,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -238,6 +258,7 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
*/
|
||||
_onDeleteGoogleFontClick: async function (ev) {
|
||||
ev.preventDefault();
|
||||
const values = {};
|
||||
|
||||
const save = await new Promise(resolve => {
|
||||
Dialog.confirm(this, _t("Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?"), {
|
||||
@@ -251,15 +272,23 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
|
||||
// Remove Google font
|
||||
const googleFontIndex = parseInt(ev.target.dataset.fontIndex);
|
||||
const googleFont = this.googleFonts[googleFontIndex];
|
||||
this.googleFonts.splice(googleFontIndex, 1);
|
||||
const isLocalFont = ev.target.dataset.localFont;
|
||||
let googleFontName;
|
||||
if (isLocalFont) {
|
||||
const googleFont = this.googleLocalFonts[googleFontIndex].split(':');
|
||||
googleFontName = googleFont[0];
|
||||
values['delete-font-attachment-id'] = googleFont[1];
|
||||
this.googleLocalFonts.splice(googleFontIndex, 1);
|
||||
} else {
|
||||
googleFontName = this.googleFonts[googleFontIndex];
|
||||
this.googleFonts.splice(googleFontIndex, 1);
|
||||
}
|
||||
|
||||
// Adapt font variable indexes to the removal
|
||||
const values = {};
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
_.each(FontFamilyPickerUserValueWidget.prototype.fontVariables, variable => {
|
||||
const value = weUtils.getCSSVariableValue(variable, style);
|
||||
if (value.substring(1, value.length - 1) === googleFont) {
|
||||
if (value.substring(1, value.length - 1) === googleFontName) {
|
||||
// If an element is using the google font being removed, reset
|
||||
// it to the theme default.
|
||||
values[variable] = 'null';
|
||||
@@ -269,6 +298,7 @@ const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
this.trigger_up('google_fonts_custo_request', {
|
||||
values: values,
|
||||
googleFonts: this.googleFonts,
|
||||
googleLocalFonts: this.googleLocalFonts,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -722,11 +752,17 @@ options.Class.include({
|
||||
_onGoogleFontsCustoRequest: function (ev) {
|
||||
const values = ev.data.values ? _.clone(ev.data.values) : {};
|
||||
const googleFonts = ev.data.googleFonts;
|
||||
const googleLocalFonts = ev.data.googleLocalFonts;
|
||||
if (googleFonts.length) {
|
||||
values['google-fonts'] = "('" + googleFonts.join("', '") + "')";
|
||||
} else {
|
||||
values['google-fonts'] = 'null';
|
||||
}
|
||||
if (googleLocalFonts.length) {
|
||||
values['google-local-fonts'] = "(" + googleLocalFonts.join(", ") + ")";
|
||||
} else {
|
||||
values['google-local-fonts'] = 'null';
|
||||
}
|
||||
this.trigger_up('snippet_edition_request', {exec: async () => {
|
||||
return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values);
|
||||
}});
|
||||
|
||||
@@ -1954,6 +1954,7 @@ $o-selected-color-palettes-names: (
|
||||
$o-base-website-values-palette: (
|
||||
'font-size-base': 1rem, // Need a set value as the value is used in bootstrap_overridden files
|
||||
'google-fonts': null,
|
||||
'google-local-fonts': null,
|
||||
|
||||
'body-image': null,
|
||||
'body-image-type': 'image', // 'image' or 'pattern'
|
||||
|
||||
@@ -230,6 +230,17 @@ $o-theme-font-configs: map-merge($o-base-theme-font-configs, $o-theme-font-confi
|
||||
));
|
||||
}
|
||||
|
||||
// Add locally hosted google fonts
|
||||
@each $font-name, $font-attach-id in (o-website-value('google-local-fonts') or ()) {
|
||||
$o-theme-font-configs: map-merge($o-theme-font-configs, (
|
||||
$font-name: (
|
||||
'family': (quote($font-name), sans-serif),
|
||||
'attachment': $font-attach-id,
|
||||
'name': quote($font-name),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// Add odoo unicode support for all fonts
|
||||
@each $font-name, $font-config in $o-theme-font-configs {
|
||||
$o-theme-font-configs: map-merge($o-theme-font-configs, (
|
||||
|
||||
@@ -8,6 +8,12 @@ $-seen-urls: ();
|
||||
@if $-url and index($-seen-urls, $-url) == null {
|
||||
$-seen-urls: append($-seen-urls, $-url);
|
||||
@import url("https://fonts.googleapis.com/css?family=#{unquote($-url)}&display=swap");
|
||||
} @else {
|
||||
$-name: o-get-font-info($alias, 'name');
|
||||
$-attachment: o-get-font-info($alias, 'attachment');
|
||||
@if $-attachment {
|
||||
@import url("/web/content/#{$-attachment}/google-font-#{unquote($-name)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
$url: map-get($font-config, 'url');
|
||||
@if $url {
|
||||
@import url("https://fonts.googleapis.com/css?family=#{unquote($url)}&display=swap");
|
||||
} @else {
|
||||
$name: map-get($font-config, 'name');
|
||||
$attachment: map-get($font-config, 'attachment');
|
||||
@if $attachment {
|
||||
@import url("/web/content/#{$attachment}/google-font-#{unquote($name)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,15 +61,21 @@
|
||||
Select one font on <a target="_blank" href="https://fonts.google.com">fonts.google.com</a> and copy paste the address of the font page here.
|
||||
</span>
|
||||
</div>
|
||||
<label class="col-form-label col-md-3" for="google_font_serve">Serve font from Google servers</label>
|
||||
<label class="o_switch col-form-label col-md-9" for="google_font_serve">
|
||||
<input type="checkbox" checked="checked" id="google_font_serve"/>
|
||||
<span/>
|
||||
</label>
|
||||
</div>
|
||||
<p>Adding a font requires a reload of the page. This will save all your changes.</p>
|
||||
</div>
|
||||
<t t-name="website.delete_google_font_btn">
|
||||
<i t-if="!local" role="button" class="text-info ms-2 fa fa-cloud" title="This font is hosted and served to your visitors by Google servers"/>
|
||||
<t t-set="delete_font_title">Delete this font</t>
|
||||
<i role="button"
|
||||
class="text-danger ms-2 fa fa-trash-o o_we_delete_google_font_btn"
|
||||
t-att-aria-label="delete_font_title"
|
||||
t-att-title="delete_font_title"
|
||||
t-att-data-local-font="local"
|
||||
t-att-data-font-index="index"/>
|
||||
</t>
|
||||
<t t-name="website.add_google_font_btn">
|
||||
|
||||
@@ -120,6 +120,9 @@
|
||||
</t>
|
||||
</t>
|
||||
<link t-if="request and website and website.is_public_user()" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
|
||||
<!-- TODO: Once we have style in DB, add this preconnect only if a
|
||||
google font is actually used. Note that if no font is used, the
|
||||
preconnect is actually not connecting to the google servers. -->
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin=""/>
|
||||
</xpath>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user