1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-01 17:38:37 +00:00

feat: add custom user JavaScript

This commit is contained in:
milhnl 2024-10-06 01:54:11 +02:00 committed by GitHub
parent f16735fd6d
commit e07203ad46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 89 additions and 32 deletions

View file

@ -942,4 +942,9 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(sql) _, err = tx.Exec(sql)
return err return err
}, },
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE users ADD COLUMN custom_js text not null default '';`
_, err = tx.Exec(sql)
return err
},
} }

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen", "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.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS", "form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
"form.prefs.label.custom_js": "Benutzerdefiniertes JS",
"form.prefs.label.entry_order": "Artikel-Sortierspalte", "form.prefs.label.entry_order": "Artikel-Sortierspalte",
"form.prefs.label.default_home_page": "Standard-Startseite", "form.prefs.label.default_home_page": "Standard-Startseite",
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung", "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων", "form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα", "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
"form.prefs.label.custom_css": "Προσαρμοσμένο CSS", "form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
"form.prefs.label.custom_js": "Προσαρμοσμένο JS",
"form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου", "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα", "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών", "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Gesture to navigate between entries", "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.show_reading_time": "Show estimated reading time for entries",
"form.prefs.label.custom_css": "Custom CSS", "form.prefs.label.custom_css": "Custom CSS",
"form.prefs.label.custom_js": "Custom JS",
"form.prefs.label.entry_order": "Entry sorting column", "form.prefs.label.entry_order": "Entry sorting column",
"form.prefs.label.default_home_page": "Default home page", "form.prefs.label.default_home_page": "Default home page",
"form.prefs.label.categories_sorting_order": "Categories sorting", "form.prefs.label.categories_sorting_order": "Categories sorting",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Gesto para navegar entre entradas", "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.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
"form.prefs.label.custom_css": "CSS personalizado", "form.prefs.label.custom_css": "CSS personalizado",
"form.prefs.label.custom_js": "JS personalizado",
"form.prefs.label.entry_order": "Columna de clasificación de artículos", "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.default_home_page": "Página de inicio por defecto",
"form.prefs.label.categories_sorting_order": "Clasificación por categorías", "form.prefs.label.categories_sorting_order": "Clasificación por categorías",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä", "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.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
"form.prefs.label.custom_css": "Mukautettu CSS", "form.prefs.label.custom_css": "Mukautettu CSS",
"form.prefs.label.custom_js": "Mukautettu JS",
"form.prefs.label.entry_order": "Lajittele sarakkeen mukaan", "form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu", "form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu", "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées", "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.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_css": "Feuille de style personnalisée",
"form.prefs.label.custom_js": "Script personnalisée",
"form.prefs.label.entry_order": "Colonne de tri des entrées", "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.default_home_page": "Page d'accueil par défaut",
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories", "form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा", "form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा",
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं", "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
"form.prefs.label.custom_css": "कस्टम सीएसएस", "form.prefs.label.custom_css": "कस्टम सीएसएस",
"form.prefs.label.custom_js": "कस्टम जेएस",
"form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम", "form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़", "form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई", "form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",

View file

@ -381,6 +381,7 @@
"form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri", "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.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
"form.prefs.label.custom_css": "Modifikasi CSS", "form.prefs.label.custom_css": "Modifikasi CSS",
"form.prefs.label.custom_js": "Modifikasi JS",
"form.prefs.label.entry_order": "Pengurutan Kolom Entri", "form.prefs.label.entry_order": "Pengurutan Kolom Entri",
"form.prefs.label.default_home_page": "Beranda Baku", "form.prefs.label.default_home_page": "Beranda Baku",
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori", "form.prefs.label.categories_sorting_order": "Pengurutan Kategori",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Gesto per navigare tra le voci", "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.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
"form.prefs.label.custom_css": "CSS personalizzati", "form.prefs.label.custom_css": "CSS personalizzati",
"form.prefs.label.custom_js": "JS personalizzati",
"form.prefs.label.entry_order": "Colonna di ordinamento delle voci", "form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
"form.prefs.label.default_home_page": "Pagina iniziale predefinita", "form.prefs.label.default_home_page": "Pagina iniziale predefinita",
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie", "form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",

View file

@ -381,6 +381,7 @@
"form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー", "form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
"form.prefs.label.custom_css": "カスタム CSS", "form.prefs.label.custom_css": "カスタム CSS",
"form.prefs.label.custom_js": "カスタム JS",
"form.prefs.label.entry_order": "記事の表示順の基準", "form.prefs.label.entry_order": "記事の表示順の基準",
"form.prefs.label.default_home_page": "デフォルトのトップページ", "form.prefs.label.default_home_page": "デフォルトのトップページ",
"form.prefs.label.categories_sorting_order": "カテゴリの表示順", "form.prefs.label.categories_sorting_order": "カテゴリの表示順",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren", "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.show_reading_time": "Toon geschatte leestijd van artikelen",
"form.prefs.label.custom_css": "Aangepaste CSS", "form.prefs.label.custom_css": "Aangepaste CSS",
"form.prefs.label.custom_js": "Aangepaste JS",
"form.prefs.label.entry_order": "Artikelen sorteren", "form.prefs.label.entry_order": "Artikelen sorteren",
"form.prefs.label.default_home_page": "Startpagina", "form.prefs.label.default_home_page": "Startpagina",
"form.prefs.label.categories_sorting_order": "Volgorde categorieën", "form.prefs.label.categories_sorting_order": "Volgorde categorieën",

View file

@ -401,6 +401,7 @@
"form.prefs.select.tap": "Podwójne wciśnięcie", "form.prefs.select.tap": "Podwójne wciśnięcie",
"form.prefs.select.swipe": "Trzepnąć", "form.prefs.select.swipe": "Trzepnąć",
"form.prefs.label.custom_css": "Niestandardowy CSS", "form.prefs.label.custom_css": "Niestandardowy CSS",
"form.prefs.label.custom_js": "Niestandardowy JS",
"form.prefs.label.entry_order": "Kolumna sortowania wpisów", "form.prefs.label.entry_order": "Kolumna sortowania wpisów",
"form.prefs.label.default_home_page": "Domyślna strona główna", "form.prefs.label.default_home_page": "Domyślna strona główna",
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii", "form.prefs.label.categories_sorting_order": "Sortowanie kategorii",

View file

@ -391,6 +391,7 @@
"form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas", "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.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
"form.prefs.label.custom_css": "CSS customizado", "form.prefs.label.custom_css": "CSS customizado",
"form.prefs.label.custom_js": "JS customizado",
"form.prefs.label.entry_order": "Coluna de Ordenação de Entrada", "form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
"form.prefs.label.default_home_page": "Página inicial predefinida", "form.prefs.label.default_home_page": "Página inicial predefinida",
"form.prefs.label.categories_sorting_order": "Classificação das categorias", "form.prefs.label.categories_sorting_order": "Classificação das categorias",

View file

@ -401,6 +401,7 @@
"form.prefs.label.gesture_nav": "Жест для перехода между статьями", "form.prefs.label.gesture_nav": "Жест для перехода между статьями",
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
"form.prefs.label.custom_css": "Пользовательский CSS", "form.prefs.label.custom_css": "Пользовательский CSS",
"form.prefs.label.custom_js": "Пользовательский JS",
"form.prefs.label.entry_order": "Столбец сортировки статей", "form.prefs.label.entry_order": "Столбец сортировки статей",
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию", "form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
"form.prefs.label.categories_sorting_order": "Сортировка категорий", "form.prefs.label.categories_sorting_order": "Сортировка категорий",

View file

@ -293,6 +293,7 @@
"form.prefs.label.categories_sorting_order": "Kategori sıralaması", "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.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_css": "Özel CSS",
"form.prefs.label.custom_js": "Özel JS",
"form.prefs.label.default_home_page": "Varsayılan ana sayfa", "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.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", "form.prefs.label.display_mode": "Progressive Web App (PWA) görüntüleme modu",

View file

@ -401,6 +401,7 @@
"form.prefs.label.gesture_nav": "Жест для переходу між записами", "form.prefs.label.gesture_nav": "Жест для переходу між записами",
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів", "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
"form.prefs.label.custom_css": "Спеціальний CSS", "form.prefs.label.custom_css": "Спеціальний CSS",
"form.prefs.label.custom_js": "Спеціальний JS",
"form.prefs.label.entry_order": "Стовпець сортування записів", "form.prefs.label.entry_order": "Стовпець сортування записів",
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням", "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
"form.prefs.label.categories_sorting_order": "Сортування за категоріями", "form.prefs.label.categories_sorting_order": "Сортування за категоріями",

View file

@ -381,6 +381,7 @@
"form.prefs.label.gesture_nav": "在条目之间导航的手势", "form.prefs.label.gesture_nav": "在条目之间导航的手势",
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
"form.prefs.label.custom_css": "自定义 CSS", "form.prefs.label.custom_css": "自定义 CSS",
"form.prefs.label.custom_js": "自定义 JS",
"form.prefs.label.entry_order": "文章排序依据", "form.prefs.label.entry_order": "文章排序依据",
"form.prefs.label.default_home_page": "默认主页", "form.prefs.label.default_home_page": "默认主页",
"form.prefs.label.categories_sorting_order": "分类排序", "form.prefs.label.categories_sorting_order": "分类排序",

View file

@ -381,6 +381,7 @@
"form.prefs.label.gesture_nav": "在條目之間導航的手勢", "form.prefs.label.gesture_nav": "在條目之間導航的手勢",
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間", "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
"form.prefs.label.custom_css": "自定義 CSS", "form.prefs.label.custom_css": "自定義 CSS",
"form.prefs.label.custom_js": "自定義 JS",
"form.prefs.label.entry_order": "文章排序依據", "form.prefs.label.entry_order": "文章排序依據",
"form.prefs.label.default_home_page": "預設主頁", "form.prefs.label.default_home_page": "預設主頁",
"form.prefs.label.categories_sorting_order": "分類排序", "form.prefs.label.categories_sorting_order": "分類排序",

View file

@ -21,6 +21,7 @@ type User struct {
EntryDirection string `json:"entry_sorting_direction"` EntryDirection string `json:"entry_sorting_direction"`
EntryOrder string `json:"entry_sorting_order"` EntryOrder string `json:"entry_sorting_order"`
Stylesheet string `json:"stylesheet"` Stylesheet string `json:"stylesheet"`
CustomJS string `json:"custom_js"`
GoogleID string `json:"google_id"` GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"` OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"` EntriesPerPage int `json:"entries_per_page"`
@ -60,6 +61,7 @@ type UserModificationRequest struct {
EntryDirection *string `json:"entry_sorting_direction"` EntryDirection *string `json:"entry_sorting_direction"`
EntryOrder *string `json:"entry_sorting_order"` EntryOrder *string `json:"entry_sorting_order"`
Stylesheet *string `json:"stylesheet"` Stylesheet *string `json:"stylesheet"`
CustomJS *string `json:"custom_js"`
GoogleID *string `json:"google_id"` GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"` OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"` EntriesPerPage *int `json:"entries_per_page"`
@ -118,6 +120,10 @@ func (u *UserModificationRequest) Patch(user *User) {
user.Stylesheet = *u.Stylesheet user.Stylesheet = *u.Stylesheet
} }
if u.CustomJS != nil {
user.CustomJS = *u.CustomJS
}
if u.GoogleID != nil { if u.GoogleID != nil {
user.GoogleID = *u.GoogleID user.GoogleID = *u.GoogleID
} }

View file

@ -83,6 +83,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
entry_swipe, entry_swipe,
gesture_nav, gesture_nav,
stylesheet, stylesheet,
custom_js,
google_id, google_id,
openid_connect_id, openid_connect_id,
display_mode, display_mode,
@ -124,6 +125,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.EntrySwipe, &user.EntrySwipe,
&user.GestureNav, &user.GestureNav,
&user.Stylesheet, &user.Stylesheet,
&user.CustomJS,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode, &user.DisplayMode,
@ -184,21 +186,22 @@ func (s *Storage) UpdateUser(user *model.User) error {
entry_swipe=$11, entry_swipe=$11,
gesture_nav=$12, gesture_nav=$12,
stylesheet=$13, stylesheet=$13,
google_id=$14, custom_js=$14,
openid_connect_id=$15, google_id=$15,
display_mode=$16, openid_connect_id=$16,
entry_order=$17, display_mode=$17,
default_reading_speed=$18, entry_order=$18,
cjk_reading_speed=$19, default_reading_speed=$19,
default_home_page=$20, cjk_reading_speed=$20,
categories_sorting_order=$21, default_home_page=$21,
mark_read_on_view=$22, categories_sorting_order=$22,
mark_read_on_media_player_completion=$23, mark_read_on_view=$23,
media_playback_rate=$24, mark_read_on_media_player_completion=$24,
block_filter_entry_rules=$25, media_playback_rate=$25,
keep_filter_entry_rules=$26 block_filter_entry_rules=$26,
keep_filter_entry_rules=$27
WHERE WHERE
id=$27 id=$28
` `
_, err = s.db.Exec( _, err = s.db.Exec(
@ -216,6 +219,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.EntrySwipe, user.EntrySwipe,
user.GestureNav, user.GestureNav,
user.Stylesheet, user.Stylesheet,
user.CustomJS,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
user.DisplayMode, user.DisplayMode,
@ -249,21 +253,22 @@ func (s *Storage) UpdateUser(user *model.User) error {
entry_swipe=$10, entry_swipe=$10,
gesture_nav=$11, gesture_nav=$11,
stylesheet=$12, stylesheet=$12,
google_id=$13, custom_js=$13,
openid_connect_id=$14, google_id=$14,
display_mode=$15, openid_connect_id=$15,
entry_order=$16, display_mode=$16,
default_reading_speed=$17, entry_order=$17,
cjk_reading_speed=$18, default_reading_speed=$18,
default_home_page=$19, cjk_reading_speed=$19,
categories_sorting_order=$20, default_home_page=$20,
mark_read_on_view=$21, categories_sorting_order=$21,
mark_read_on_media_player_completion=$22, mark_read_on_view=$22,
media_playback_rate=$23, mark_read_on_media_player_completion=$23,
block_filter_entry_rules=$24, media_playback_rate=$24,
keep_filter_entry_rules=$25 block_filter_entry_rules=$25,
keep_filter_entry_rules=$26
WHERE WHERE
id=$26 id=$27
` `
_, err := s.db.Exec( _, err := s.db.Exec(
@ -280,6 +285,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.EntrySwipe, user.EntrySwipe,
user.GestureNav, user.GestureNav,
user.Stylesheet, user.Stylesheet,
user.CustomJS,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
user.DisplayMode, user.DisplayMode,
@ -332,6 +338,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
gesture_nav, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
custom_js,
google_id, google_id,
openid_connect_id, openid_connect_id,
display_mode, display_mode,
@ -371,6 +378,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
gesture_nav, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
custom_js,
google_id, google_id,
openid_connect_id, openid_connect_id,
display_mode, display_mode,
@ -410,6 +418,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
gesture_nav, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
custom_js,
google_id, google_id,
openid_connect_id, openid_connect_id,
display_mode, display_mode,
@ -456,6 +465,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.gesture_nav, u.gesture_nav,
u.last_login_at, u.last_login_at,
u.stylesheet, u.stylesheet,
u.custom_js,
u.google_id, u.google_id,
u.openid_connect_id, u.openid_connect_id,
u.display_mode, u.display_mode,
@ -496,6 +506,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.GestureNav, &user.GestureNav,
&user.LastLoginAt, &user.LastLoginAt,
&user.Stylesheet, &user.Stylesheet,
&user.CustomJS,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode, &user.DisplayMode,
@ -608,6 +619,7 @@ func (s *Storage) Users() (model.Users, error) {
gesture_nav, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
custom_js,
google_id, google_id,
openid_connect_id, openid_connect_id,
display_mode, display_mode,
@ -649,6 +661,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.GestureNav, &user.GestureNav,
&user.LastLoginAt, &user.LastLoginAt,
&user.Stylesheet, &user.Stylesheet,
&user.CustomJS,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode, &user.DisplayMode,

View file

@ -56,6 +56,9 @@ func (f *funcMap) Map() template.FuncMap {
"safeCSS": func(str string) template.CSS { "safeCSS": func(str string) template.CSS {
return template.CSS(str) return template.CSS(str)
}, },
"safeJS": func(str string) template.JS {
return template.JS(str)
},
"noescape": func(str string) template.HTML { "noescape": func(str string) template.HTML {
return template.HTML(str) return template.HTML(str)
}, },

View file

@ -34,10 +34,15 @@
<link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}"> <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
{{ if and .user .user.Stylesheet }} {{ if .user }}
{{ $stylesheetNonce := nonce }} {{ $cspNonce := nonce }}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self' 'nonce-{{ $stylesheetNonce }}'; require-trusted-types-for 'script'; trusted-types ttpolicy;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; style-src 'self'{{ if .user.Stylesheet }} 'nonce-{{ $cspNonce }}'{{ end }}{{ if .user.CustomJS }}; script-src 'self' 'nonce-{{ $cspNonce }}'{{ end }}; require-trusted-types-for 'script'; trusted-types ttpolicy;">
<style nonce="{{ $stylesheetNonce }}">{{ .user.Stylesheet | safeCSS }}</style> {{ if .user.Stylesheet }}
<style nonce="{{ $cspNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
{{ end }}
{{ if .user.CustomJS }}
<script type="module" nonce="{{ $cspNonce }}">{{ .user.CustomJS | safeJS }}</script>
{{ end }}
{{ else }} {{ else }}
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types ttpolicy;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data:; media-src *; frame-src *; require-trusted-types-for 'script'; trusted-types ttpolicy;">
{{ end }} {{ end }}

View file

@ -210,6 +210,9 @@
<label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label> <label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
<textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea> <textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
<label for="form-custom-js">{{t "form.prefs.label.custom_js" }}</label>
<textarea id="form-custom-js" name="custom_js" cols="40" rows="10" spellcheck="false">{{ .form.CustomJS }}</textarea>
<div class="buttons"> <div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button> <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div> </div>

View file

@ -36,6 +36,7 @@ type SettingsForm struct {
KeyboardShortcuts bool KeyboardShortcuts bool
ShowReadingTime bool ShowReadingTime bool
CustomCSS string CustomCSS string
CustomJS string
EntrySwipe bool EntrySwipe bool
GestureNav string GestureNav string
DisplayMode string DisplayMode string
@ -99,6 +100,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.KeyboardShortcuts = s.KeyboardShortcuts user.KeyboardShortcuts = s.KeyboardShortcuts
user.ShowReadingTime = s.ShowReadingTime user.ShowReadingTime = s.ShowReadingTime
user.Stylesheet = s.CustomCSS user.Stylesheet = s.CustomCSS
user.CustomJS = s.CustomJS
user.EntrySwipe = s.EntrySwipe user.EntrySwipe = s.EntrySwipe
user.GestureNav = s.GestureNav user.GestureNav = s.GestureNav
user.DisplayMode = s.DisplayMode user.DisplayMode = s.DisplayMode
@ -180,6 +182,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1", KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
ShowReadingTime: r.FormValue("show_reading_time") == "1", ShowReadingTime: r.FormValue("show_reading_time") == "1",
CustomCSS: r.FormValue("custom_css"), CustomCSS: r.FormValue("custom_css"),
CustomJS: r.FormValue("custom_js"),
EntrySwipe: r.FormValue("entry_swipe") == "1", EntrySwipe: r.FormValue("entry_swipe") == "1",
GestureNav: r.FormValue("gesture_nav"), GestureNav: r.FormValue("gesture_nav"),
DisplayMode: r.FormValue("display_mode"), DisplayMode: r.FormValue("display_mode"),

View file

@ -33,6 +33,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
KeyboardShortcuts: user.KeyboardShortcuts, KeyboardShortcuts: user.KeyboardShortcuts,
ShowReadingTime: user.ShowReadingTime, ShowReadingTime: user.ShowReadingTime,
CustomCSS: user.Stylesheet, CustomCSS: user.Stylesheet,
CustomJS: user.CustomJS,
EntrySwipe: user.EntrySwipe, EntrySwipe: user.EntrySwipe,
GestureNav: user.GestureNav, GestureNav: user.GestureNav,
DisplayMode: user.DisplayMode, DisplayMode: user.DisplayMode,