diff --git a/addons/website/models/assets.py b/addons/website/models/assets.py index 14da04ee1f0..de62ddcb83b 100644 --- a/addons/website/models/assets.py +++ b/addons/website/models/assets.py @@ -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') diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 2c21bee43fe..c0455dd0362 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -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); }}); diff --git a/addons/website/static/src/scss/primary_variables.scss b/addons/website/static/src/scss/primary_variables.scss index edf9a3762da..93811fb0cee 100644 --- a/addons/website/static/src/scss/primary_variables.scss +++ b/addons/website/static/src/scss/primary_variables.scss @@ -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' diff --git a/addons/website/static/src/scss/secondary_variables.scss b/addons/website/static/src/scss/secondary_variables.scss index 08f55012a6c..e82aac2eef8 100644 --- a/addons/website/static/src/scss/secondary_variables.scss +++ b/addons/website/static/src/scss/secondary_variables.scss @@ -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, ( diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index d99ebe2ff82..9f09f6e54d4 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -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)}"); + } } } diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss index fa2c3c2e13c..672469557a0 100644 --- a/addons/website/static/src/scss/website.wysiwyg.scss +++ b/addons/website/static/src/scss/website.wysiwyg.scss @@ -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)}"); + } } } diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index b2c12f67da0..32fbafee5aa 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -61,15 +61,21 @@ Select one font on fonts.google.com and copy paste the address of the font page here. + + -
Adding a font requires a reload of the page. This will save all your changes.