From e555e442fba0a9ca3c8cf67444e8c42f79db359a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 5 Oct 2024 20:37:30 -0700 Subject: [PATCH] feat: add new settings option to allow external fonts --- client/model.go | 2 + internal/api/api_integration_test.go | 53 ++++++++++++++ internal/database/migrations.go | 5 ++ internal/locale/translations/de_DE.json | 5 +- internal/locale/translations/el_EL.json | 5 +- internal/locale/translations/en_US.json | 5 +- internal/locale/translations/es_ES.json | 5 +- internal/locale/translations/fi_FI.json | 5 +- internal/locale/translations/fr_FR.json | 5 +- internal/locale/translations/hi_IN.json | 3 + internal/locale/translations/id_ID.json | 5 +- internal/locale/translations/it_IT.json | 5 +- internal/locale/translations/ja_JP.json | 5 +- internal/locale/translations/nl_NL.json | 5 +- internal/locale/translations/pl_PL.json | 5 +- internal/locale/translations/pt_BR.json | 5 +- internal/locale/translations/ru_RU.json | 5 +- internal/locale/translations/tr_TR.json | 5 +- internal/locale/translations/uk_UA.json | 5 +- internal/locale/translations/zh_CN.json | 5 +- internal/locale/translations/zh_TW.json | 5 +- internal/model/user.go | 6 ++ internal/storage/user.go | 71 +++++++++++-------- .../template/templates/common/layout.html | 20 +++--- .../template/templates/views/settings.html | 4 ++ internal/ui/form/settings.go | 10 +++ internal/ui/settings_show.go | 1 + internal/ui/settings_update.go | 1 + internal/ui/static/css/common.css | 4 +- internal/validator/user.go | 6 ++ internal/validator/validator.go | 24 +++++++ internal/validator/validator_test.go | 18 +++++ 32 files changed, 257 insertions(+), 56 deletions(-) diff --git a/client/model.go b/client/model.go index f0583ae486c..0a6cd4d1016 100644 --- a/client/model.go +++ b/client/model.go @@ -45,6 +45,7 @@ type User struct { MediaPlaybackRate float64 `json:"media_playback_rate"` BlockFilterEntryRules string `json:"block_filter_entry_rules"` KeepFilterEntryRules string `json:"keep_filter_entry_rules"` + ExternalFontHosts string `json:"external_font_hosts"` } func (u User) String() string { @@ -88,6 +89,7 @@ type UserModificationRequest struct { MediaPlaybackRate *float64 `json:"media_playback_rate"` BlockFilterEntryRules *string `json:"block_filter_entry_rules"` KeepFilterEntryRules *string `json:"keep_filter_entry_rules"` + ExternalFontHosts *string `json:"external_font_hosts"` } // Users represents a list of users. diff --git a/internal/api/api_integration_test.go b/internal/api/api_integration_test.go index 1a95cf08bf3..fe172ce587d 100644 --- a/internal/api/api_integration_test.go +++ b/internal/api/api_integration_test.go @@ -592,6 +592,59 @@ func TestUpdateUserEndpointByChangingDefaultTheme(t *testing.T) { } } +func TestUpdateUserEndpointByChangingExternalFonts(t *testing.T) { + testConfig := newIntegrationTestConfig() + if !testConfig.isConfigured() { + t.Skip(skipIntegrationTestsMessage) + } + + adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword) + regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false) + if err != nil { + t.Fatal(err) + } + defer adminClient.DeleteUser(regularTestUser.ID) + + regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword) + + userUpdateRequest := &miniflux.UserModificationRequest{ + ExternalFontHosts: miniflux.SetOptionalField(" fonts.example.org "), + } + + updatedUser, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest) + if err != nil { + t.Fatal(err) + } + + if updatedUser.ExternalFontHosts != "fonts.example.org" { + t.Fatalf(`Invalid external font hosts, got "%v"`, updatedUser.ExternalFontHosts) + } +} + +func TestUpdateUserEndpointByChangingExternalFontsWithInvalidValue(t *testing.T) { + testConfig := newIntegrationTestConfig() + if !testConfig.isConfigured() { + t.Skip(skipIntegrationTestsMessage) + } + + adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword) + regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false) + if err != nil { + t.Fatal(err) + } + defer adminClient.DeleteUser(regularTestUser.ID) + + regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword) + + userUpdateRequest := &miniflux.UserModificationRequest{ + ExternalFontHosts: miniflux.SetOptionalField("'self' *"), + } + + if _, err := regularUserClient.UpdateUser(regularTestUser.ID, userUpdateRequest); err == nil { + t.Fatal(`Updating the user with an invalid external font host should raise an error`) + } +} + func TestUpdateUserEndpointByChangingCustomJS(t *testing.T) { testConfig := newIntegrationTestConfig() if !testConfig.isConfigured() { diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 2c0939dabe6..2c7ea8b2838 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -947,4 +947,9 @@ var migrations = []func(tx *sql.Tx) error{ _, err = tx.Exec(sql) return err }, + func(tx *sql.Tx) (err error) { + sql := `ALTER TABLE users ADD COLUMN external_font_hosts text not null default '';` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json index 82c508ee88e..370a9f3bfca 100644 --- a/internal/locale/translations/de_DE.json +++ b/internal/locale/translations/de_DE.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen", "form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen", "form.prefs.label.custom_css": "Benutzerdefiniertes CSS", - "form.prefs.label.custom_js": "Benutzerdefiniertes JS", + "form.prefs.label.custom_js": "Benutzerdefiniertes JavaScript", "form.prefs.label.entry_order": "Artikel-Sortierspalte", "form.prefs.label.default_home_page": "Standard-Startseite", "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen", "form.prefs.fieldset.reader_settings": "Reader-Einstellungen", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "Externe Schriftarten-Hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML Datei", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json index 5f80554632f..1585128cdf1 100644 --- a/internal/locale/translations/el_EL.json +++ b/internal/locale/translations/el_EL.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων", "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα", "form.prefs.label.custom_css": "Προσαρμοσμένο CSS", - "form.prefs.label.custom_js": "Προσαρμοσμένο JS", + "form.prefs.label.custom_js": "Προσαρμοσμένο JavaScript", "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου", "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα", "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Αρχείο OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json index 8c7f65ae0b8..eaf4d1e89d1 100644 --- a/internal/locale/translations/en_US.json +++ b/internal/locale/translations/en_US.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Gesture to navigate between entries", "form.prefs.label.show_reading_time": "Show estimated reading time for entries", "form.prefs.label.custom_css": "Custom CSS", - "form.prefs.label.custom_js": "Custom JS", + "form.prefs.label.custom_js": "Custom JavaScript", "form.prefs.label.entry_order": "Entry sorting column", "form.prefs.label.default_home_page": "Default home page", "form.prefs.label.categories_sorting_order": "Categories sorting", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML file", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json index 1e76897eeb2..51cd0f230aa 100644 --- a/internal/locale/translations/es_ES.json +++ b/internal/locale/translations/es_ES.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Gesto para navegar entre entradas", "form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos", "form.prefs.label.custom_css": "CSS personalizado", - "form.prefs.label.custom_js": "JS personalizado", + "form.prefs.label.custom_js": "JavaScript personalizado", "form.prefs.label.entry_order": "Columna de clasificación de artículos", "form.prefs.label.default_home_page": "Página de inicio por defecto", "form.prefs.label.categories_sorting_order": "Clasificación por categorías", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Ajustes de la autentificación", "form.prefs.fieldset.reader_settings": "Ajustes del lector", "form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Archivo OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Guardar artículos en Betula", diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json index e543dfd3819..05945de8690 100644 --- a/internal/locale/translations/fi_FI.json +++ b/internal/locale/translations/fi_FI.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä", "form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika", "form.prefs.label.custom_css": "Mukautettu CSS", - "form.prefs.label.custom_js": "Mukautettu JS", + "form.prefs.label.custom_js": "Mukautettu JavaScript", "form.prefs.label.entry_order": "Lajittele sarakkeen mukaan", "form.prefs.label.default_home_page": "Oletusarvoinen etusivu", "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML-tiedosto", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json index d54e689c97a..8fdf4f2e9ab 100644 --- a/internal/locale/translations/fr_FR.json +++ b/internal/locale/translations/fr_FR.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées", "form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles", "form.prefs.label.custom_css": "Feuille de style personnalisée", - "form.prefs.label.custom_js": "Script personnalisée", + "form.prefs.label.custom_js": "Code JavaScript personnalisé", "form.prefs.label.entry_order": "Colonne de tri des entrées", "form.prefs.label.default_home_page": "Page d'accueil par défaut", "form.prefs.label.categories_sorting_order": "Colonne de tri des catégories", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Paramètres d'authentification", "form.prefs.fieldset.reader_settings": "Paramètres du lecteur", "form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements", + "form.prefs.label.external_font_hosts": "Polices externes autorisées", + "form.prefs.help.external_font_hosts": "Liste de domaine externes autorisés, séparés par des espaces. Par exemple : « fonts.gstatic.com fonts.googleapis.com ».", + "error.settings_invalid_domain_list": "Liste de domaines invalide. Veuillez fournir une liste de domaines séparés par des espaces.", "form.import.label.file": "Fichier OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Sauvegarder les entrées vers Betula", diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json index 026f3e4c08a..746eed9be5f 100644 --- a/internal/locale/translations/hi_IN.json +++ b/internal/locale/translations/hi_IN.json @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "ओपीएमएल फ़ाइल", "form.import.label.url": "यूआरएल", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json index 75cef1fc46a..e92516e05e0 100644 --- a/internal/locale/translations/id_ID.json +++ b/internal/locale/translations/id_ID.json @@ -381,7 +381,7 @@ "form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri", "form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel", "form.prefs.label.custom_css": "Modifikasi CSS", - "form.prefs.label.custom_js": "Modifikasi JS", + "form.prefs.label.custom_js": "Modifikasi JavaScript", "form.prefs.label.entry_order": "Pengurutan Kolom Entri", "form.prefs.label.default_home_page": "Beranda Baku", "form.prefs.label.categories_sorting_order": "Pengurutan Kategori", @@ -393,6 +393,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Berkas OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json index 930b80ba944..d754f5e50d3 100644 --- a/internal/locale/translations/it_IT.json +++ b/internal/locale/translations/it_IT.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Gesto per navigare tra le voci", "form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli", "form.prefs.label.custom_css": "CSS personalizzati", - "form.prefs.label.custom_js": "JS personalizzati", + "form.prefs.label.custom_js": "JavaScript personalizzati", "form.prefs.label.entry_order": "Colonna di ordinamento delle voci", "form.prefs.label.default_home_page": "Pagina iniziale predefinita", "form.prefs.label.categories_sorting_order": "Ordinamento delle categorie", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "File OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json index ad4cceeee2c..0b0a8f5f9c0 100644 --- a/internal/locale/translations/ja_JP.json +++ b/internal/locale/translations/ja_JP.json @@ -381,7 +381,7 @@ "form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.custom_css": "カスタム CSS", - "form.prefs.label.custom_js": "カスタム JS", + "form.prefs.label.custom_js": "カスタム JavaScript", "form.prefs.label.entry_order": "記事の表示順の基準", "form.prefs.label.default_home_page": "デフォルトのトップページ", "form.prefs.label.categories_sorting_order": "カテゴリの表示順", @@ -393,6 +393,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML ファイル", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json index 0e278c1a1ac..fc7ef956aef 100644 --- a/internal/locale/translations/nl_NL.json +++ b/internal/locale/translations/nl_NL.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren", "form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen", "form.prefs.label.custom_css": "Aangepaste CSS", - "form.prefs.label.custom_js": "Aangepaste JS", + "form.prefs.label.custom_js": "Aangepaste JavaScript", "form.prefs.label.entry_order": "Artikelen sorteren", "form.prefs.label.default_home_page": "Startpagina", "form.prefs.label.categories_sorting_order": "Volgorde categorieën", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen", "form.prefs.fieldset.reader_settings": "Lees Instellingen", "form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML-bestand", "form.import.label.url": "URL", "form.integration.betula_activate": "Artikelen opslaan in Betula", diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json index 0dd6ae04bd5..4e60d0279dd 100644 --- a/internal/locale/translations/pl_PL.json +++ b/internal/locale/translations/pl_PL.json @@ -401,7 +401,7 @@ "form.prefs.select.tap": "Podwójne wciśnięcie", "form.prefs.select.swipe": "Trzepnąć", "form.prefs.label.custom_css": "Niestandardowy CSS", - "form.prefs.label.custom_js": "Niestandardowy JS", + "form.prefs.label.custom_js": "Niestandardowy JavaScript", "form.prefs.label.entry_order": "Kolumna sortowania wpisów", "form.prefs.label.default_home_page": "Domyślna strona główna", "form.prefs.label.categories_sorting_order": "Sortowanie kategorii", @@ -413,6 +413,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Plik OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json index 0e10d728498..a1dc2b12467 100644 --- a/internal/locale/translations/pt_BR.json +++ b/internal/locale/translations/pt_BR.json @@ -391,7 +391,7 @@ "form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas", "form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos", "form.prefs.label.custom_css": "CSS customizado", - "form.prefs.label.custom_js": "JS customizado", + "form.prefs.label.custom_js": "JavaScript customizado", "form.prefs.label.entry_order": "Coluna de Ordenação de Entrada", "form.prefs.label.default_home_page": "Página inicial predefinida", "form.prefs.label.categories_sorting_order": "Classificação das categorias", @@ -403,6 +403,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Arquivo OPML", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json index a040d8797c0..4c6b26bc7de 100644 --- a/internal/locale/translations/ru_RU.json +++ b/internal/locale/translations/ru_RU.json @@ -401,7 +401,7 @@ "form.prefs.label.gesture_nav": "Жест для перехода между статьями", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.custom_css": "Пользовательский CSS", - "form.prefs.label.custom_js": "Пользовательский JS", + "form.prefs.label.custom_js": "Пользовательский JavaScript", "form.prefs.label.entry_order": "Столбец сортировки статей", "form.prefs.label.default_home_page": "Домашняя страница по умолчанию", "form.prefs.label.categories_sorting_order": "Сортировка категорий", @@ -413,6 +413,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML файл", "form.import.label.url": "Ссылка", "form.integration.betula_activate": "Сохранять статьи в Бетулу", diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json index c9f55c5dd82..91ac11682be 100644 --- a/internal/locale/translations/tr_TR.json +++ b/internal/locale/translations/tr_TR.json @@ -293,7 +293,7 @@ "form.prefs.label.categories_sorting_order": "Kategori sıralaması", "form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)", "form.prefs.label.custom_css": "Özel CSS", - "form.prefs.label.custom_js": "Özel JS", + "form.prefs.label.custom_js": "Özel JavaScript", "form.prefs.label.default_home_page": "Varsayılan ana sayfa", "form.prefs.label.default_reading_speed": "Diğer diller için okuma hızı (dakika başına kelime)", "form.prefs.label.display_mode": "Progressive Web App (PWA) görüntüleme modu", @@ -325,6 +325,9 @@ "form.prefs.select.swipe": "Kaydırma", "form.prefs.select.tap": "Çift dokunma", "form.prefs.select.unread_count": "Okunmamış sayısı", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.submit.loading": "Yükleniyor...", "form.submit.saving": "Kaydediliyor...", "form.user.label.admin": "Yönetici", diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json index 91027cb77b3..7ca464184c6 100644 --- a/internal/locale/translations/uk_UA.json +++ b/internal/locale/translations/uk_UA.json @@ -401,7 +401,7 @@ "form.prefs.label.gesture_nav": "Жест для переходу між записами", "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів", "form.prefs.label.custom_css": "Спеціальний CSS", - "form.prefs.label.custom_js": "Спеціальний JS", + "form.prefs.label.custom_js": "Спеціальний JavaScript", "form.prefs.label.entry_order": "Стовпець сортування записів", "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням", "form.prefs.label.categories_sorting_order": "Сортування за категоріями", @@ -413,6 +413,9 @@ "form.prefs.fieldset.authentication_settings": "Authentication Settings", "form.prefs.fieldset.reader_settings": "Reader Settings", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "Файл OPML", "form.import.label.url": "URL-адреса", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json index 2942847e517..8309e7a752d 100644 --- a/internal/locale/translations/zh_CN.json +++ b/internal/locale/translations/zh_CN.json @@ -381,7 +381,7 @@ "form.prefs.label.gesture_nav": "在条目之间导航的手势", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.custom_css": "自定义 CSS", - "form.prefs.label.custom_js": "自定义 JS", + "form.prefs.label.custom_js": "自定义 JavaScript", "form.prefs.label.entry_order": "文章排序依据", "form.prefs.label.default_home_page": "默认主页", "form.prefs.label.categories_sorting_order": "分类排序", @@ -393,6 +393,9 @@ "form.prefs.fieldset.authentication_settings": "用户认证设置", "form.prefs.fieldset.reader_settings": "阅读器设置", "form.prefs.fieldset.global_feed_settings": "全局订阅源设置", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML 文件", "form.import.label.url": "URL", "form.integration.betula_activate": "保存文章到 Betula", diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json index b9505aac4c3..3c7c46004d0 100644 --- a/internal/locale/translations/zh_TW.json +++ b/internal/locale/translations/zh_TW.json @@ -381,7 +381,7 @@ "form.prefs.label.gesture_nav": "在條目之間導航的手勢", "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間", "form.prefs.label.custom_css": "自定義 CSS", - "form.prefs.label.custom_js": "自定義 JS", + "form.prefs.label.custom_js": "自定義 JavaScript", "form.prefs.label.entry_order": "文章排序依據", "form.prefs.label.default_home_page": "預設主頁", "form.prefs.label.categories_sorting_order": "分類排序", @@ -393,6 +393,9 @@ "form.prefs.fieldset.authentication_settings": "使用者認證設定", "form.prefs.fieldset.reader_settings": "閱讀器設定", "form.prefs.fieldset.global_feed_settings": "Global Feed Settings", + "form.prefs.label.external_font_hosts": "External font hosts", + "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".", + "error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.", "form.import.label.file": "OPML 檔案", "form.import.label.url": "URL", "form.integration.betula_activate": "Save entries to Betula", diff --git a/internal/model/user.go b/internal/model/user.go index ba14b99da19..ad070904e55 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -22,6 +22,7 @@ type User struct { EntryOrder string `json:"entry_sorting_order"` Stylesheet string `json:"stylesheet"` CustomJS string `json:"custom_js"` + ExternalFontHosts string `json:"external_font_hosts"` GoogleID string `json:"google_id"` OpenIDConnectID string `json:"openid_connect_id"` EntriesPerPage int `json:"entries_per_page"` @@ -62,6 +63,7 @@ type UserModificationRequest struct { EntryOrder *string `json:"entry_sorting_order"` Stylesheet *string `json:"stylesheet"` CustomJS *string `json:"custom_js"` + ExternalFontHosts *string `json:"external_font_hosts"` GoogleID *string `json:"google_id"` OpenIDConnectID *string `json:"openid_connect_id"` EntriesPerPage *int `json:"entries_per_page"` @@ -124,6 +126,10 @@ func (u *UserModificationRequest) Patch(user *User) { user.CustomJS = *u.CustomJS } + if u.ExternalFontHosts != nil { + user.ExternalFontHosts = *u.ExternalFontHosts + } + if u.GoogleID != nil { user.GoogleID = *u.GoogleID } diff --git a/internal/storage/user.go b/internal/storage/user.go index 43baf0e1d54..39a019ca51c 100644 --- a/internal/storage/user.go +++ b/internal/storage/user.go @@ -84,6 +84,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m gesture_nav, stylesheet, custom_js, + external_font_hosts, google_id, openid_connect_id, display_mode, @@ -126,6 +127,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.GestureNav, &user.Stylesheet, &user.CustomJS, + &user.ExternalFontHosts, &user.GoogleID, &user.OpenIDConnectID, &user.DisplayMode, @@ -165,6 +167,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m // UpdateUser updates a user. func (s *Storage) UpdateUser(user *model.User) error { + user.ExternalFontHosts = strings.TrimSpace(user.ExternalFontHosts) + if user.Password != "" { hashedPassword, err := crypto.HashPassword(user.Password) if err != nil { @@ -187,21 +191,22 @@ func (s *Storage) UpdateUser(user *model.User) error { gesture_nav=$12, stylesheet=$13, custom_js=$14, - google_id=$15, - openid_connect_id=$16, - display_mode=$17, - entry_order=$18, - default_reading_speed=$19, - cjk_reading_speed=$20, - default_home_page=$21, - categories_sorting_order=$22, - mark_read_on_view=$23, - mark_read_on_media_player_completion=$24, - media_playback_rate=$25, - block_filter_entry_rules=$26, - keep_filter_entry_rules=$27 + external_font_hosts=$15, + google_id=$16, + openid_connect_id=$167, + display_mode=$18, + entry_order=$19, + default_reading_speed=$20, + cjk_reading_speed=$21, + default_home_page=$22, + categories_sorting_order=$23, + mark_read_on_view=$24, + mark_read_on_media_player_completion=$25, + media_playback_rate=$26, + block_filter_entry_rules=$27, + keep_filter_entry_rules=$28 WHERE - id=$28 + id=$29 ` _, err = s.db.Exec( @@ -220,6 +225,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.GestureNav, user.Stylesheet, user.CustomJS, + user.ExternalFontHosts, user.GoogleID, user.OpenIDConnectID, user.DisplayMode, @@ -254,21 +260,22 @@ func (s *Storage) UpdateUser(user *model.User) error { gesture_nav=$11, stylesheet=$12, custom_js=$13, - google_id=$14, - openid_connect_id=$15, - display_mode=$16, - entry_order=$17, - default_reading_speed=$18, - cjk_reading_speed=$19, - default_home_page=$20, - categories_sorting_order=$21, - mark_read_on_view=$22, - mark_read_on_media_player_completion=$23, - media_playback_rate=$24, - block_filter_entry_rules=$25, - keep_filter_entry_rules=$26 + external_font_hosts=$14, + google_id=$15, + openid_connect_id=$16, + display_mode=$17, + entry_order=$18, + default_reading_speed=$19, + cjk_reading_speed=$20, + default_home_page=$21, + categories_sorting_order=$22, + mark_read_on_view=$23, + mark_read_on_media_player_completion=$24, + media_playback_rate=$25, + block_filter_entry_rules=$26, + keep_filter_entry_rules=$27 WHERE - id=$27 + id=$28 ` _, err := s.db.Exec( @@ -286,6 +293,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.GestureNav, user.Stylesheet, user.CustomJS, + user.ExternalFontHosts, user.GoogleID, user.OpenIDConnectID, user.DisplayMode, @@ -339,6 +347,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { last_login_at, stylesheet, custom_js, + external_font_hosts, google_id, openid_connect_id, display_mode, @@ -379,6 +388,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { last_login_at, stylesheet, custom_js, + external_font_hosts, google_id, openid_connect_id, display_mode, @@ -419,6 +429,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { last_login_at, stylesheet, custom_js, + external_font_hosts, google_id, openid_connect_id, display_mode, @@ -466,6 +477,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.last_login_at, u.stylesheet, u.custom_js, + u.external_font_hosts, u.google_id, u.openid_connect_id, u.display_mode, @@ -507,6 +519,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.LastLoginAt, &user.Stylesheet, &user.CustomJS, + &user.ExternalFontHosts, &user.GoogleID, &user.OpenIDConnectID, &user.DisplayMode, @@ -620,6 +633,7 @@ func (s *Storage) Users() (model.Users, error) { last_login_at, stylesheet, custom_js, + external_font_hosts, google_id, openid_connect_id, display_mode, @@ -662,6 +676,7 @@ func (s *Storage) Users() (model.Users, error) { &user.LastLoginAt, &user.Stylesheet, &user.CustomJS, + &user.ExternalFontHosts, &user.GoogleID, &user.OpenIDConnectID, &user.DisplayMode, diff --git a/internal/template/templates/common/layout.html b/internal/template/templates/common/layout.html index 55a6263f2d7..13c8c652556 100644 --- a/internal/template/templates/common/layout.html +++ b/internal/template/templates/common/layout.html @@ -35,16 +35,18 @@ {{ if .user }} - {{ $cspNonce := nonce }} - - {{ if .user.Stylesheet }} - - {{ end }} - {{ if .user.CustomJS }} - - {{ end }} + {{ $cspNonce := nonce }} + + + {{ if .user.Stylesheet }} + + {{ end }} + + {{ if .user.CustomJS }} + + {{ end }} {{ else }} - + {{ end }} diff --git a/internal/template/templates/views/settings.html b/internal/template/templates/views/settings.html index 535c5a1a53a..c584e02a994 100644 --- a/internal/template/templates/views/settings.html +++ b/internal/template/templates/views/settings.html @@ -210,6 +210,10 @@

{{ t "page.settings.title" }}

+ + +
{{t "form.prefs.help.external_font_hosts" }}
+ diff --git a/internal/ui/form/settings.go b/internal/ui/form/settings.go index cf18bd2ec84..1b9e48ddeae 100644 --- a/internal/ui/form/settings.go +++ b/internal/ui/form/settings.go @@ -10,6 +10,7 @@ import ( "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/locale" "miniflux.app/v2/internal/model" + "miniflux.app/v2/internal/validator" ) // MarkReadBehavior list all possible behaviors for automatically marking an entry as read @@ -37,6 +38,7 @@ type SettingsForm struct { ShowReadingTime bool CustomCSS string CustomJS string + ExternalFontHosts string EntrySwipe bool GestureNav string DisplayMode string @@ -101,6 +103,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { user.ShowReadingTime = s.ShowReadingTime user.Stylesheet = s.CustomCSS user.CustomJS = s.CustomJS + user.ExternalFontHosts = s.ExternalFontHosts user.EntrySwipe = s.EntrySwipe user.GestureNav = s.GestureNav user.DisplayMode = s.DisplayMode @@ -148,6 +151,12 @@ func (s *SettingsForm) Validate() *locale.LocalizedError { return locale.NewLocalizedError("error.settings_media_playback_rate_range") } + if s.ExternalFontHosts != "" { + if !validator.IsValidDomainList(s.ExternalFontHosts) { + return locale.NewLocalizedError("error.settings_invalid_domain_list") + } + } + return nil } @@ -183,6 +192,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm { ShowReadingTime: r.FormValue("show_reading_time") == "1", CustomCSS: r.FormValue("custom_css"), CustomJS: r.FormValue("custom_js"), + ExternalFontHosts: r.FormValue("external_font_hosts"), EntrySwipe: r.FormValue("entry_swipe") == "1", GestureNav: r.FormValue("gesture_nav"), DisplayMode: r.FormValue("display_mode"), diff --git a/internal/ui/settings_show.go b/internal/ui/settings_show.go index 72e1f5ab20c..179b98025f1 100644 --- a/internal/ui/settings_show.go +++ b/internal/ui/settings_show.go @@ -34,6 +34,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { ShowReadingTime: user.ShowReadingTime, CustomCSS: user.Stylesheet, CustomJS: user.CustomJS, + ExternalFontHosts: user.ExternalFontHosts, EntrySwipe: user.EntrySwipe, GestureNav: user.GestureNav, DisplayMode: user.DisplayMode, diff --git a/internal/ui/settings_update.go b/internal/ui/settings_update.go index 0e03752d5e2..be99adb548f 100644 --- a/internal/ui/settings_update.go +++ b/internal/ui/settings_update.go @@ -85,6 +85,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) { MediaPlaybackRate: model.OptionalNumber(settingsForm.MediaPlaybackRate), BlockFilterEntryRules: model.OptionalString(settingsForm.BlockFilterEntryRules), KeepFilterEntryRules: model.OptionalString(settingsForm.KeepFilterEntryRules), + ExternalFontHosts: model.OptionalString(settingsForm.ExternalFontHosts), } if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil { diff --git a/internal/ui/static/css/common.css b/internal/ui/static/css/common.css index 6ffaa8bf091..2bd3a535986 100644 --- a/internal/ui/static/css/common.css +++ b/internal/ui/static/css/common.css @@ -427,7 +427,6 @@ input[type="number"] { line-height: 20px; width: 250px; font-size: 99%; - margin-bottom: 10px; margin-top: 5px; appearance: none; } @@ -448,7 +447,8 @@ input[type="number"]:focus { } input[type="checkbox"] { - margin-bottom: 15px; + margin-top: 10px; + margin-bottom: 10px; } textarea { diff --git a/internal/validator/user.go b/internal/validator/user.go index 2365ee61da4..2e79785b407 100644 --- a/internal/validator/user.go +++ b/internal/validator/user.go @@ -123,6 +123,12 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod } } + if changes.ExternalFontHosts != nil { + if !IsValidDomainList(*changes.ExternalFontHosts) { + return locale.NewLocalizedError("error.settings_invalid_domain_list") + } + } + return nil } diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 63fe75f0882..9b3cfd908b3 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -7,8 +7,11 @@ import ( "fmt" "net/url" "regexp" + "strings" ) +var domainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) + // ValidateRange makes sure the offset/limit values are valid. func ValidateRange(offset, limit int) error { if offset < 0 { @@ -43,3 +46,24 @@ func IsValidURL(absoluteURL string) bool { _, err := url.ParseRequestURI(absoluteURL) return err == nil } + +func IsValidDomain(domain string) bool { + domain = strings.ToLower(domain) + + if len(domain) < 1 || len(domain) > 253 { + return false + } + + return domainRegex.MatchString(domain) +} + +func IsValidDomainList(value string) bool { + domains := strings.Split(strings.TrimSpace(value), " ") + for _, domain := range domains { + if !IsValidDomain(domain) { + return false + } + } + + return true +} diff --git a/internal/validator/validator_test.go b/internal/validator/validator_test.go index 0a51973be70..7121a111f99 100644 --- a/internal/validator/validator_test.go +++ b/internal/validator/validator_test.go @@ -59,3 +59,21 @@ func TestIsValidRegex(t *testing.T) { } } } + +func TestIsValidDomain(t *testing.T) { + scenarios := map[string]bool{ + "example.org": true, + "example": false, + "example.": false, + "example..": false, + "mail.example.com:443": false, + "*.example.com": false, + } + + for domain, expected := range scenarios { + result := IsValidDomain(domain) + if result != expected { + t.Errorf(`Unexpected result, got %v instead of %v`, result, expected) + } + } +}