Переглянути джерело

feat: add custom user JavaScript

milhnl 1 рік тому
батько
коміт
e07203ad46

+ 5 - 0
internal/database/migrations.go

@@ -942,4 +942,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 custom_js text not null default '';`
+		_, err = tx.Exec(sql)
+		return err
+	},
 }

+ 1 - 0
internal/locale/translations/de_DE.json

@@ -391,6 +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.entry_order": "Artikel-Sortierspalte",
     "form.prefs.label.default_home_page": "Standard-Startseite",
     "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",

+ 1 - 0
internal/locale/translations/el_EL.json

@@ -391,6 +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.entry_order": "Στήλη ταξινόμησης εισόδου",
     "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
     "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",

+ 1 - 0
internal/locale/translations/en_US.json

@@ -391,6 +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.entry_order": "Entry sorting column",
     "form.prefs.label.default_home_page": "Default home page",
     "form.prefs.label.categories_sorting_order": "Categories sorting",

+ 1 - 0
internal/locale/translations/es_ES.json

@@ -391,6 +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.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",

+ 1 - 0
internal/locale/translations/fi_FI.json

@@ -391,6 +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.entry_order": "Lajittele sarakkeen mukaan",
     "form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
     "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",

+ 1 - 0
internal/locale/translations/fr_FR.json

@@ -391,6 +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.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",

+ 1 - 0
internal/locale/translations/hi_IN.json

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

+ 1 - 0
internal/locale/translations/id_ID.json

@@ -381,6 +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.entry_order": "Pengurutan Kolom Entri",
     "form.prefs.label.default_home_page": "Beranda Baku",
     "form.prefs.label.categories_sorting_order": "Pengurutan Kategori",

+ 1 - 0
internal/locale/translations/it_IT.json

@@ -391,6 +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.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",

+ 1 - 0
internal/locale/translations/ja_JP.json

@@ -381,6 +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.entry_order": "記事の表示順の基準",
     "form.prefs.label.default_home_page": "デフォルトのトップページ",
     "form.prefs.label.categories_sorting_order": "カテゴリの表示順",

+ 1 - 0
internal/locale/translations/nl_NL.json

@@ -391,6 +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.entry_order": "Artikelen sorteren",
     "form.prefs.label.default_home_page": "Startpagina",
     "form.prefs.label.categories_sorting_order": "Volgorde categorieën",

+ 1 - 0
internal/locale/translations/pl_PL.json

@@ -401,6 +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.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",

+ 1 - 0
internal/locale/translations/pt_BR.json

@@ -391,6 +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.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",

+ 1 - 0
internal/locale/translations/ru_RU.json

@@ -401,6 +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.entry_order": "Столбец сортировки статей",
     "form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
     "form.prefs.label.categories_sorting_order": "Сортировка категорий",

+ 1 - 0
internal/locale/translations/tr_TR.json

@@ -293,6 +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.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",

+ 1 - 0
internal/locale/translations/uk_UA.json

@@ -401,6 +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.entry_order": "Стовпець сортування записів",
     "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
     "form.prefs.label.categories_sorting_order": "Сортування за категоріями",

+ 1 - 0
internal/locale/translations/zh_CN.json

@@ -381,6 +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.entry_order": "文章排序依据",
     "form.prefs.label.default_home_page": "默认主页",
     "form.prefs.label.categories_sorting_order": "分类排序",

+ 1 - 0
internal/locale/translations/zh_TW.json

@@ -381,6 +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.entry_order": "文章排序依據",
     "form.prefs.label.default_home_page": "預設主頁",
     "form.prefs.label.categories_sorting_order": "分類排序",

+ 6 - 0
internal/model/user.go

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

+ 41 - 28
internal/storage/user.go

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

+ 3 - 0
internal/template/functions.go

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

+ 9 - 4
internal/template/templates/common/layout.html

@@ -34,10 +34,15 @@
 
     <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .theme "checksum" .theme_checksum }}">
 
-    {{ if and .user .user.Stylesheet }}
-    {{ $stylesheetNonce := 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;">
-    <style nonce="{{ $stylesheetNonce }}">{{ .user.Stylesheet | safeCSS }}</style>
+    {{ if .user }}
+    {{ $cspNonce := nonce }}
+    <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;">
+    {{ 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 }}
     <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 }}

+ 3 - 0
internal/template/templates/views/settings.html

@@ -210,6 +210,9 @@
         <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>
 
+        <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">
             <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
         </div>

+ 3 - 0
internal/ui/form/settings.go

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

+ 1 - 0
internal/ui/settings_show.go

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