diff --git a/client/model.go b/client/model.go
index 1e064d26..1f7a28ea 100644
--- a/client/model.go
+++ b/client/model.go
@@ -17,36 +17,37 @@ const (
// User represents a user in the system.
type User struct {
- ID int64 `json:"id"`
- Username string `json:"username"`
- Password string `json:"password,omitempty"`
- IsAdmin bool `json:"is_admin"`
- Theme string `json:"theme"`
- Language string `json:"language"`
- Timezone string `json:"timezone"`
- 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"`
- KeyboardShortcuts bool `json:"keyboard_shortcuts"`
- ShowReadingTime bool `json:"show_reading_time"`
- EntrySwipe bool `json:"entry_swipe"`
- GestureNav string `json:"gesture_nav"`
- LastLoginAt *time.Time `json:"last_login_at"`
- DisplayMode string `json:"display_mode"`
- DefaultReadingSpeed int `json:"default_reading_speed"`
- CJKReadingSpeed int `json:"cjk_reading_speed"`
- DefaultHomePage string `json:"default_home_page"`
- CategoriesSortingOrder string `json:"categories_sorting_order"`
- MarkReadOnView bool `json:"mark_read_on_view"`
- 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"`
- AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
+ ID int64 `json:"id"`
+ Username string `json:"username"`
+ Password string `json:"password,omitempty"`
+ IsAdmin bool `json:"is_admin"`
+ Theme string `json:"theme"`
+ Language string `json:"language"`
+ Timezone string `json:"timezone"`
+ 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"`
+ KeyboardShortcuts bool `json:"keyboard_shortcuts"`
+ ShowReadingTime bool `json:"show_reading_time"`
+ EntrySwipe bool `json:"entry_swipe"`
+ GestureNav string `json:"gesture_nav"`
+ LastLoginAt *time.Time `json:"last_login_at"`
+ DisplayMode string `json:"display_mode"`
+ DefaultReadingSpeed int `json:"default_reading_speed"`
+ CJKReadingSpeed int `json:"cjk_reading_speed"`
+ DefaultHomePage string `json:"default_home_page"`
+ CategoriesSortingOrder string `json:"categories_sorting_order"`
+ MarkReadOnView bool `json:"mark_read_on_view"`
+ 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"`
+ AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
+ OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
}
func (u User) String() string {
@@ -64,34 +65,35 @@ type UserCreationRequest struct {
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
- Username *string `json:"username"`
- Password *string `json:"password"`
- IsAdmin *bool `json:"is_admin"`
- Theme *string `json:"theme"`
- Language *string `json:"language"`
- Timezone *string `json:"timezone"`
- 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"`
- KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
- ShowReadingTime *bool `json:"show_reading_time"`
- EntrySwipe *bool `json:"entry_swipe"`
- GestureNav *string `json:"gesture_nav"`
- DisplayMode *string `json:"display_mode"`
- DefaultReadingSpeed *int `json:"default_reading_speed"`
- CJKReadingSpeed *int `json:"cjk_reading_speed"`
- DefaultHomePage *string `json:"default_home_page"`
- CategoriesSortingOrder *string `json:"categories_sorting_order"`
- MarkReadOnView *bool `json:"mark_read_on_view"`
- 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"`
- AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
+ Username *string `json:"username"`
+ Password *string `json:"password"`
+ IsAdmin *bool `json:"is_admin"`
+ Theme *string `json:"theme"`
+ Language *string `json:"language"`
+ Timezone *string `json:"timezone"`
+ 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"`
+ KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
+ ShowReadingTime *bool `json:"show_reading_time"`
+ EntrySwipe *bool `json:"entry_swipe"`
+ GestureNav *string `json:"gesture_nav"`
+ DisplayMode *string `json:"display_mode"`
+ DefaultReadingSpeed *int `json:"default_reading_speed"`
+ CJKReadingSpeed *int `json:"cjk_reading_speed"`
+ DefaultHomePage *string `json:"default_home_page"`
+ CategoriesSortingOrder *string `json:"categories_sorting_order"`
+ MarkReadOnView *bool `json:"mark_read_on_view"`
+ 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"`
+ AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
+ OpenExternalLinksInNewTab *bool `json:"open_external_links_in_new_tab"`
}
// Users represents a list of users.
diff --git a/internal/database/migrations.go b/internal/database/migrations.go
index 70c11792..ba16eb2f 100644
--- a/internal/database/migrations.go
+++ b/internal/database/migrations.go
@@ -1086,4 +1086,8 @@ var migrations = []func(tx *sql.Tx, driver string) error{
_, err = tx.Exec(sql)
return
},
+ func(tx *sql.Tx, _ string) (err error) {
+ _, err = tx.Exec(`ALTER TABLE users ADD COLUMN open_external_links_in_new_tab bool default 't'`)
+ return err
+ },
}
diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json
index a7ecea18..d839a218 100644
--- a/internal/locale/translations/de_DE.json
+++ b/internal/locale/translations/de_DE.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
"form.prefs.label.mark_read_on_view_or_media_completion": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden. Audio/Video bei 90%% Wiedergabe als gelesen markieren",
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
+ "form.prefs.label.open_external_links_in_new_tab": "Externe Links in einem neuen Tab öffnen (fügt target=\"_blank\" zu Links hinzu)",
"form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
"form.prefs.label.theme": "Thema",
"form.prefs.label.timezone": "Zeitzone",
diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json
index bb672e5e..5b6bff6b 100644
--- a/internal/locale/translations/el_EL.json
+++ b/internal/locale/translations/el_EL.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
"form.prefs.label.mark_read_on_view_or_media_completion": "Σήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή. Για ήχο/βίντεο, σήμανση ως αναγνωσμένου στο 90%% ολοκλήρωσης",
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
+ "form.prefs.label.open_external_links_in_new_tab": "Άνοιγμα εξωτερικών συνδέσμων σε νέα καρτέλα (προσθέτει target=\"_blank\" στους συνδέσμους)",
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
"form.prefs.label.theme": "Θέμα",
"form.prefs.label.timezone": "Ζώνη Ώρας",
diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json
index 8df8b741..1961ef7d 100644
--- a/internal/locale/translations/en_US.json
+++ b/internal/locale/translations/en_US.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
+ "form.prefs.label.open_external_links_in_new_tab": "Open external links in a new tab (adds target=\"_blank\" to links)",
"form.prefs.label.show_reading_time": "Show estimated reading time for entries",
"form.prefs.label.theme": "Theme",
"form.prefs.label.timezone": "Timezone",
diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json
index 7c4cb163..a1fe1e3f 100644
--- a/internal/locale/translations/es_ES.json
+++ b/internal/locale/translations/es_ES.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marcar las entradas como leídas cuando se vean. Para audio/video, marcar como leído al 90%% de finalización",
"form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
+ "form.prefs.label.open_external_links_in_new_tab": "Abrir enlaces externos en una nueva pestaña (agrega target=\"_blank\" a los enlaces)",
"form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
"form.prefs.label.theme": "Tema",
"form.prefs.label.timezone": "Zona horaria",
diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json
index c9ff7b8a..17517d6a 100644
--- a/internal/locale/translations/fi_FI.json
+++ b/internal/locale/translations/fi_FI.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
+ "form.prefs.label.open_external_links_in_new_tab": "Avaa ulkoiset linkit uuteen välilehteen (lisää target=\"_blank\" linkkeihin)",
"form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
"form.prefs.label.theme": "Teema",
"form.prefs.label.timezone": "Aikavyöhyke",
diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json
index a1e25317..582f62aa 100644
--- a/internal/locale/translations/fr_FR.json
+++ b/internal/locale/translations/fr_FR.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%",
"form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
+ "form.prefs.label.open_external_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet (ajoute target=\"_blank\" aux liens)",
"form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
"form.prefs.label.theme": "Thème",
"form.prefs.label.timezone": "Fuseau horaire",
diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json
index d6b9ff13..e82a9eef 100644
--- a/internal/locale/translations/hi_IN.json
+++ b/internal/locale/translations/hi_IN.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
+ "form.prefs.label.open_external_links_in_new_tab": "बाहरी लिंक को एक नए टैब में खोलें (लिंक में target=\"_blank\" जोड़ता है)",
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
"form.prefs.label.theme": "थीम",
"form.prefs.label.timezone": "समय क्षेत्र",
diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json
index 6f05d919..1515b97a 100644
--- a/internal/locale/translations/id_ID.json
+++ b/internal/locale/translations/id_ID.json
@@ -353,6 +353,7 @@
"form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
"form.prefs.label.mark_read_on_view_or_media_completion": "Tandai entri sebagai telah dibaca ketika dilihat. Untuk audio/video, tandai sebagai telah dibaca ketika sudah 90% didengar/ditonton.",
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
+ "form.prefs.label.open_external_links_in_new_tab": "Buka tautan eksternal di tab baru (menambahkan target=\"_blank\" ke tautan)",
"form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
"form.prefs.label.theme": "Tema",
"form.prefs.label.timezone": "Zona Waktu",
diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json
index ebd46064..bd20df67 100644
--- a/internal/locale/translations/it_IT.json
+++ b/internal/locale/translations/it_IT.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
+ "form.prefs.label.open_external_links_in_new_tab": "Apri i link esterni in una nuova scheda (aggiunge target=\"_blank\" ai link)",
"form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
"form.prefs.label.theme": "Tema",
"form.prefs.label.timezone": "Fuso orario",
diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json
index a868d969..8611dd89 100644
--- a/internal/locale/translations/ja_JP.json
+++ b/internal/locale/translations/ja_JP.json
@@ -353,6 +353,7 @@
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
+ "form.prefs.label.open_external_links_in_new_tab": "外部リンクを新しいタブで開く(リンクに target=\"_blank\" を追加)",
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
"form.prefs.label.theme": "テーマ",
"form.prefs.label.timezone": "タイムゾーン",
diff --git a/internal/locale/translations/nan_Latn_pehoeji.json b/internal/locale/translations/nan_Latn_pehoeji.json
index ec8f25d5..a205342a 100644
--- a/internal/locale/translations/nan_Latn_pehoeji.json
+++ b/internal/locale/translations/nan_Latn_pehoeji.json
@@ -353,6 +353,7 @@
"form.prefs.label.mark_read_on_view": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè",
"form.prefs.label.mark_read_on_view_or_media_completion": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè, m̄-koh nā-sī im-sìn, sī-sìn tio̍h tī hòng-sàng kàu 90%% ê si-chun chiah lâi chù",
"form.prefs.label.media_playback_rate": "Im-sìn, sī-sìn pàng ê sok-tō͘",
+ "form.prefs.label.open_external_links_in_new_tab": "Chhiau-chhē gōa-pō͘ liân-kiat sī tī sin ê ia̍h phah khui (kā liân-kiat chhē target=\"_blank\")",
"form.prefs.label.show_reading_time": "Hián-sī siau-sit àn-sǹg ài gōa-kú lâi tha̍k",
"form.prefs.label.theme": "Chú-tôe",
"form.prefs.label.timezone": "Sî-khu",
diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json
index 902830a2..3f933043 100644
--- a/internal/locale/translations/nl_NL.json
+++ b/internal/locale/translations/nl_NL.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Markeer artikelen automatisch als gelezen wanneer ze worden bekeken",
"form.prefs.label.mark_read_on_view_or_media_completion": "Markeer artikelen als gelezen wanneer ze worden bekeken. Voor audio/video, markeer als gelezen bij 90%% voltooiing",
"form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
+ "form.prefs.label.open_external_links_in_new_tab": "Open externe links in een nieuw tabblad (voegt target=\"_blank\" toe aan links)",
"form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
"form.prefs.label.theme": "Thema",
"form.prefs.label.timezone": "Tijdzone",
diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json
index 49e58849..e7306d90 100644
--- a/internal/locale/translations/pl_PL.json
+++ b/internal/locale/translations/pl_PL.json
@@ -359,6 +359,7 @@
"form.prefs.label.mark_read_on_view": "Automatycznie oznacz wpisy jako przeczytane podczas przeglądania",
"form.prefs.label.mark_read_on_view_or_media_completion": "Oznacz wpisy jako przeczytane po wyświetleniu. W przypadku audio i wideo oznacz jako przeczytane po ukończeniu 90%%",
"form.prefs.label.media_playback_rate": "Szybkość odtwarzania audio i wideo",
+ "form.prefs.label.open_external_links_in_new_tab": "Otwieraj linki zewnętrzne w nowej karcie (dodaje target=\"_blank\" do linków)",
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania wpisów",
"form.prefs.label.theme": "Wygląd",
"form.prefs.label.timezone": "Strefa czasowa",
diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json
index e8d564c8..5d9039ae 100644
--- a/internal/locale/translations/pt_BR.json
+++ b/internal/locale/translations/pt_BR.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marcar itens como lidos quando visualizados. Para áudio/vídeo, marcar como lido em 90%% de conclusão",
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
+ "form.prefs.label.open_external_links_in_new_tab": "Abrir links externos em uma nova aba (adiciona target=\"_blank\" aos links)",
"form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
"form.prefs.label.theme": "Tema",
"form.prefs.label.timezone": "Fuso horário",
diff --git a/internal/locale/translations/ro_RO.json b/internal/locale/translations/ro_RO.json
index 509fa47b..40f581f5 100644
--- a/internal/locale/translations/ro_RO.json
+++ b/internal/locale/translations/ro_RO.json
@@ -359,6 +359,7 @@
"form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut",
"form.prefs.label.media_playback_rate": "Viteza de rulare audio/video",
+ "form.prefs.label.open_external_links_in_new_tab": "Deschide linkurile externe într-o filă nouă (adaugă target=\"_blank\" la linkuri)",
"form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
"form.prefs.label.theme": "Temă",
"form.prefs.label.timezone": "Fus orar",
diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json
index 1228c58c..64f17db2 100644
--- a/internal/locale/translations/ru_RU.json
+++ b/internal/locale/translations/ru_RU.json
@@ -359,6 +359,7 @@
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
"form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения",
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
+ "form.prefs.label.open_external_links_in_new_tab": "Открывать внешние ссылки в новой вкладке (добавляет target=\"_blank\" к ссылкам)",
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
"form.prefs.label.theme": "Тема",
"form.prefs.label.timezone": "Часовой пояс",
diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json
index c4a85611..61e1b6d2 100644
--- a/internal/locale/translations/tr_TR.json
+++ b/internal/locale/translations/tr_TR.json
@@ -356,6 +356,7 @@
"form.prefs.label.mark_read_on_view": "Makaleler görüntülendiğinde otomatik olarak okundu olarak işaretle",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "Ses/video oynatma hızı",
+ "form.prefs.label.open_external_links_in_new_tab": "Harici bağlantıları yeni bir sekmede aç (bağlantılara target=\"_blank\" ekler)",
"form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
"form.prefs.label.theme": "Tema",
"form.prefs.label.timezone": "Saat Dilimi",
diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json
index 7aafb14a..322feec2 100644
--- a/internal/locale/translations/uk_UA.json
+++ b/internal/locale/translations/uk_UA.json
@@ -359,6 +359,7 @@
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
+ "form.prefs.label.open_external_links_in_new_tab": "Відкривати зовнішні посилання у новій вкладці (додає target=\"_blank\" до посилань)",
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
"form.prefs.label.theme": "Тема",
"form.prefs.label.timezone": "Часовий пояс",
diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json
index 310ce2b8..e6ec8397 100644
--- a/internal/locale/translations/zh_CN.json
+++ b/internal/locale/translations/zh_CN.json
@@ -353,6 +353,7 @@
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
+ "form.prefs.label.open_external_links_in_new_tab": "在新标签页中打开外部链接(为链接添加 target=\"_blank\")",
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
"form.prefs.label.theme": "主题",
"form.prefs.label.timezone": "时区",
diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json
index 778afece..ac207948 100644
--- a/internal/locale/translations/zh_TW.json
+++ b/internal/locale/translations/zh_TW.json
@@ -353,6 +353,7 @@
"form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀",
"form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記",
"form.prefs.label.media_playback_rate": "音訊/視訊播放速度",
+ "form.prefs.label.open_external_links_in_new_tab": "在新分頁中開啟外部連結(為連結加上 target=\"_blank\")",
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
"form.prefs.label.theme": "主題",
"form.prefs.label.timezone": "時區",
diff --git a/internal/model/user.go b/internal/model/user.go
index 5c1fa911..a16db615 100644
--- a/internal/model/user.go
+++ b/internal/model/user.go
@@ -42,6 +42,7 @@ type User struct {
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
+ OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
}
// UserCreationRequest represents the request to create a user.
@@ -84,6 +85,7 @@ type UserModificationRequest struct {
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
+ OpenExternalLinksInNewTab *bool `json:"open_external_links_in_new_tab"`
}
// Patch updates the User object with the modification request.
@@ -203,6 +205,10 @@ func (u *UserModificationRequest) Patch(user *User) {
if u.AlwaysOpenExternalLinks != nil {
user.AlwaysOpenExternalLinks = *u.AlwaysOpenExternalLinks
}
+
+ if u.OpenExternalLinksInNewTab != nil {
+ user.OpenExternalLinksInNewTab = *u.OpenExternalLinksInNewTab
+ }
}
// UseTimezone converts last login date to the given timezone.
diff --git a/internal/reader/processor/processor.go b/internal/reader/processor/processor.go
index e2099d2c..0915d33c 100644
--- a/internal/reader/processor/processor.go
+++ b/internal/reader/processor/processor.go
@@ -125,7 +125,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, userID int64,
}
// The sanitizer should always run at the end of the process to make sure unsafe HTML is filtered out.
- entry.Content = sanitizer.Sanitize(pageBaseURL, entry.Content)
+ entry.Content = sanitizer.SanitizeHTML(pageBaseURL, entry.Content, &sanitizer.SanitizerOptions{OpenLinksInNewTab: user.OpenExternalLinksInNewTab})
updateEntryReadingTime(store, feed, entry, entryIsNew, user)
@@ -181,7 +181,7 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User)
}
rewrite.Rewriter(rewrittenEntryURL, entry, entry.Feed.RewriteRules)
- entry.Content = sanitizer.Sanitize(pageBaseURL, entry.Content)
+ entry.Content = sanitizer.SanitizeHTML(pageBaseURL, entry.Content, &sanitizer.SanitizerOptions{OpenLinksInNewTab: user.OpenExternalLinksInNewTab})
return nil
}
diff --git a/internal/reader/sanitizer/sanitizer.go b/internal/reader/sanitizer/sanitizer.go
index 994f660e..28b9d94b 100644
--- a/internal/reader/sanitizer/sanitizer.go
+++ b/internal/reader/sanitizer/sanitizer.go
@@ -112,14 +112,23 @@ var (
}
)
-// Sanitize returns safe HTML.
-func Sanitize(baseURL, input string) string {
+type SanitizerOptions struct {
+ OpenLinksInNewTab bool
+}
+
+func SanitizeHTMLWithDefaultOptions(baseURL, rawHTML string) string {
+ return SanitizeHTML(baseURL, rawHTML, &SanitizerOptions{
+ OpenLinksInNewTab: true,
+ })
+}
+
+func SanitizeHTML(baseURL, rawHTML string, sanitizerOptions *SanitizerOptions) string {
var buffer strings.Builder
var tagStack []string
var parentTag string
var blockedStack []string
- tokenizer := html.NewTokenizer(strings.NewReader(input))
+ tokenizer := html.NewTokenizer(strings.NewReader(rawHTML))
for {
if tokenizer.Next() == html.ErrorToken {
err := tokenizer.Err()
@@ -166,7 +175,7 @@ func Sanitize(baseURL, input string) string {
}
if len(blockedStack) == 0 && isValidTag(tagName) {
- attrNames, htmlAttributes := sanitizeAttributes(baseURL, tagName, token.Attr)
+ attrNames, htmlAttributes := sanitizeAttributes(baseURL, tagName, token.Attr, sanitizerOptions)
if hasRequiredAttributes(tagName, attrNames) {
if len(attrNames) > 0 {
// Rewrite the start tag with allowed attributes.
@@ -194,7 +203,7 @@ func Sanitize(baseURL, input string) string {
continue
}
if len(blockedStack) == 0 && isValidTag(tagName) {
- attrNames, htmlAttributes := sanitizeAttributes(baseURL, tagName, token.Attr)
+ attrNames, htmlAttributes := sanitizeAttributes(baseURL, tagName, token.Attr, sanitizerOptions)
if hasRequiredAttributes(tagName, attrNames) {
if len(attrNames) > 0 {
buffer.WriteString("<" + tagName + " " + htmlAttributes + "/>")
@@ -207,7 +216,7 @@ func Sanitize(baseURL, input string) string {
}
}
-func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([]string, string) {
+func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute, sanitizerOptions *SanitizerOptions) ([]string, string) {
var htmlAttrs, attrNames []string
var err error
var isImageLargerThanLayout bool
@@ -269,7 +278,7 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
}
if !isAnchorLink {
- extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName)
+ extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName, sanitizerOptions)
if len(extraAttrNames) > 0 {
attrNames = append(attrNames, extraAttrNames...)
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
@@ -279,10 +288,16 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
return attrNames, strings.Join(htmlAttrs, " ")
}
-func getExtraAttributes(tagName string) ([]string, []string) {
+func getExtraAttributes(tagName string, sanitizerOptions *SanitizerOptions) ([]string, []string) {
switch tagName {
case "a":
- return []string{"rel", "target", "referrerpolicy"}, []string{`rel="noopener noreferrer"`, `target="_blank"`, `referrerpolicy="no-referrer"`}
+ attributeNames := []string{"rel", "referrerpolicy"}
+ htmlAttributes := []string{`rel="noopener noreferrer"`, `referrerpolicy="no-referrer"`}
+ if sanitizerOptions.OpenLinksInNewTab {
+ attributeNames = append(attributeNames, "target")
+ htmlAttributes = append(htmlAttributes, `target="_blank"`)
+ }
+ return attributeNames, htmlAttributes
case "video", "audio":
return []string{"controls"}, []string{"controls"}
case "iframe":
diff --git a/internal/reader/sanitizer/sanitizer_test.go b/internal/reader/sanitizer/sanitizer_test.go
index 5cd49353..2e4d6424 100644
--- a/internal/reader/sanitizer/sanitizer_test.go
+++ b/internal/reader/sanitizer/sanitizer_test.go
@@ -33,7 +33,7 @@ func BenchmarkSanitize(b *testing.B) {
}
for range b.N {
for _, v := range testCases {
- Sanitize(v[0], v[1])
+ SanitizeHTMLWithDefaultOptions(v[0], v[1])
}
}
}
@@ -46,7 +46,7 @@ func FuzzSanitizer(f *testing.F) {
i++
}
- out := Sanitize("", orig)
+ out := SanitizeHTMLWithDefaultOptions("", orig)
tok = html.NewTokenizer(strings.NewReader(out))
j := 0
@@ -62,7 +62,7 @@ func FuzzSanitizer(f *testing.F) {
func TestValidInput(t *testing.T) {
input := `
This is a text with an image:
.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if input != output {
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
@@ -72,7 +72,7 @@ func TestValidInput(t *testing.T) {
func TestImgWithWidthAndHeightAttribute(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -82,7 +82,7 @@ func TestImgWithWidthAndHeightAttribute(t *testing.T) {
func TestImgWithWidthAndHeightAttributeLargerThanMinifluxLayout(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -92,7 +92,7 @@ func TestImgWithWidthAndHeightAttributeLargerThanMinifluxLayout(t *testing.T) {
func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -102,7 +102,7 @@ func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
func TestImgWithTextDataURL(t *testing.T) {
input := `
`
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -112,7 +112,7 @@ func TestImgWithTextDataURL(t *testing.T) {
func TestImgWithDataURL(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -122,7 +122,7 @@ func TestImgWithDataURL(t *testing.T) {
func TestImgWithSrcsetAttribute(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -132,7 +132,7 @@ func TestImgWithSrcsetAttribute(t *testing.T) {
func TestImgWithSrcsetAndNoSrcAttribute(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -142,7 +142,7 @@ func TestImgWithSrcsetAndNoSrcAttribute(t *testing.T) {
func TestSourceWithSrcsetAndMedia(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -152,7 +152,7 @@ func TestSourceWithSrcsetAndMedia(t *testing.T) {
func TestMediumImgWithSrcset(t *testing.T) {
input := `
`
expected := `
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected {
t.Errorf(`Wrong output: %s`, output)
@@ -161,7 +161,7 @@ func TestMediumImgWithSrcset(t *testing.T) {
func TestSelfClosingTags(t *testing.T) {
input := `This
is a text
with an image:
.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if input != output {
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
@@ -170,7 +170,7 @@ func TestSelfClosingTags(t *testing.T) {
func TestTable(t *testing.T) {
input := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if input != output {
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
@@ -179,8 +179,8 @@ func TestTable(t *testing.T) {
func TestRelativeURL(t *testing.T) {
input := `This link is relative and this image:
`
- expected := `This link is relative and this image:
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is relative and this image:
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -189,8 +189,8 @@ func TestRelativeURL(t *testing.T) {
func TestProtocolRelativeURL(t *testing.T) {
input := `This link is relative.`
- expected := `This link is relative.`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is relative.`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -200,7 +200,7 @@ func TestProtocolRelativeURL(t *testing.T) {
func TestInvalidTag(t *testing.T) {
input := `My invalid tag.
`
expected := `My invalid tag.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -210,7 +210,7 @@ func TestInvalidTag(t *testing.T) {
func TestVideoTag(t *testing.T) {
input := `My valid .
`
expected := `My valid .
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -220,7 +220,7 @@ func TestVideoTag(t *testing.T) {
func TestAudioAndSourceTag(t *testing.T) {
input := `My music .
`
expected := `My music .
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -230,7 +230,7 @@ func TestAudioAndSourceTag(t *testing.T) {
func TestUnknownTag(t *testing.T) {
input := `My invalid tag.
`
expected := `My invalid tag.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -240,7 +240,7 @@ func TestUnknownTag(t *testing.T) {
func TestInvalidNestedTag(t *testing.T) {
input := `My invalid tag with some valid tag.
`
expected := `My invalid tag with some valid tag.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -250,7 +250,7 @@ func TestInvalidNestedTag(t *testing.T) {
func TestInvalidIFrame(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.com/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.com/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -260,7 +260,27 @@ func TestInvalidIFrame(t *testing.T) {
func TestIFrameWithChildElements(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.com/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.com/", input)
+
+ if expected != output {
+ t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
+ }
+}
+
+func TestLinkWithTarget(t *testing.T) {
+ input := `This link is an anchor
`
+ expected := `This link is an anchor
`
+ output := SanitizeHTML("http://example.org/", input, &SanitizerOptions{OpenLinksInNewTab: true})
+
+ if expected != output {
+ t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
+ }
+}
+
+func TestLinkWithNoTarget(t *testing.T) {
+ input := `This link is an anchor
`
+ expected := `This link is an anchor
`
+ output := SanitizeHTML("http://example.org/", input, &SanitizerOptions{OpenLinksInNewTab: false})
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -270,7 +290,7 @@ func TestIFrameWithChildElements(t *testing.T) {
func TestAnchorLink(t *testing.T) {
input := `This link is an anchor
`
expected := `This link is an anchor
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -280,7 +300,7 @@ func TestAnchorLink(t *testing.T) {
func TestInvalidURLScheme(t *testing.T) {
input := `This link is not valid
`
expected := `This link is not valid
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -289,8 +309,8 @@ func TestInvalidURLScheme(t *testing.T) {
func TestAPTURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -299,8 +319,8 @@ func TestAPTURIScheme(t *testing.T) {
func TestBitcoinURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -309,8 +329,8 @@ func TestBitcoinURIScheme(t *testing.T) {
func TestCallToURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -319,16 +339,16 @@ func TestCallToURIScheme(t *testing.T) {
func TestFeedURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -337,8 +357,8 @@ func TestFeedURIScheme(t *testing.T) {
func TestGeoURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -347,16 +367,16 @@ func TestGeoURIScheme(t *testing.T) {
func TestItunesURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -365,8 +385,8 @@ func TestItunesURIScheme(t *testing.T) {
func TestMagnetURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -375,8 +395,8 @@ func TestMagnetURIScheme(t *testing.T) {
func TestMailtoURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -385,24 +405,24 @@ func TestMailtoURIScheme(t *testing.T) {
func TestNewsURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -411,8 +431,8 @@ func TestNewsURIScheme(t *testing.T) {
func TestRTMPURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -421,16 +441,16 @@ func TestRTMPURIScheme(t *testing.T) {
func TestSIPURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -439,8 +459,8 @@ func TestSIPURIScheme(t *testing.T) {
func TestSkypeURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -449,8 +469,8 @@ func TestSkypeURIScheme(t *testing.T) {
func TestSpotifyURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -459,8 +479,8 @@ func TestSpotifyURIScheme(t *testing.T) {
func TestSteamURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -469,16 +489,16 @@ func TestSteamURIScheme(t *testing.T) {
func TestSubversionURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
input = `This link is valid
`
- expected = `This link is valid
`
- output = Sanitize("http://example.org/", input)
+ expected = `This link is valid
`
+ output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -487,8 +507,8 @@ func TestSubversionURIScheme(t *testing.T) {
func TestTelURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -497,8 +517,8 @@ func TestTelURIScheme(t *testing.T) {
func TestWebcalURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -507,8 +527,8 @@ func TestWebcalURIScheme(t *testing.T) {
func TestXMPPURIScheme(t *testing.T) {
input := `This link is valid
`
- expected := `This link is valid
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link is valid
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -518,7 +538,7 @@ func TestXMPPURIScheme(t *testing.T) {
func TestBlacklistedLink(t *testing.T) {
input := `This image is not valid 
`
expected := `This image is not valid
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -527,8 +547,8 @@ func TestBlacklistedLink(t *testing.T) {
func TestLinkWithTrackers(t *testing.T) {
input := `This link has trackers Test
`
- expected := `This link has trackers Test
`
- output := Sanitize("http://example.org/", input)
+ expected := `This link has trackers Test
`
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -538,7 +558,7 @@ func TestLinkWithTrackers(t *testing.T) {
func TestImageSrcWithTrackers(t *testing.T) {
input := `This image has trackers 
`
expected := `This image has trackers 
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -548,7 +568,7 @@ func TestImageSrcWithTrackers(t *testing.T) {
func TestPixelTracker(t *testing.T) {
input := `
and 
`
expected := ` and
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -558,7 +578,7 @@ func TestPixelTracker(t *testing.T) {
func TestXmlEntities(t *testing.T) {
input := `echo "test" > /etc/hosts
`
expected := `echo "test" > /etc/hosts
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -568,7 +588,7 @@ func TestXmlEntities(t *testing.T) {
func TestEspaceAttributes(t *testing.T) {
input := `test | `
expected := `test | `
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -578,7 +598,7 @@ func TestEspaceAttributes(t *testing.T) {
func TestReplaceYoutubeURL(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -588,7 +608,7 @@ func TestReplaceYoutubeURL(t *testing.T) {
func TestReplaceSecureYoutubeURL(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -598,7 +618,7 @@ func TestReplaceSecureYoutubeURL(t *testing.T) {
func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -608,7 +628,7 @@ func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -618,7 +638,7 @@ func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -639,7 +659,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -649,7 +669,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
func TestReplaceIframeVimedoDNTURL(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -659,7 +679,7 @@ func TestReplaceIframeVimedoDNTURL(t *testing.T) {
func TestReplaceNoScript(t *testing.T) {
input := `Before paragraph.
After paragraph.
`
expected := `Before paragraph.
After paragraph.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -669,7 +689,7 @@ func TestReplaceNoScript(t *testing.T) {
func TestReplaceScript(t *testing.T) {
input := `Before paragraph.
After paragraph.
`
expected := `Before paragraph.
After paragraph.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -679,7 +699,7 @@ func TestReplaceScript(t *testing.T) {
func TestReplaceStyle(t *testing.T) {
input := `Before paragraph.
After paragraph.
`
expected := `Before paragraph.
After paragraph.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -689,7 +709,7 @@ func TestReplaceStyle(t *testing.T) {
func TestHiddenParagraph(t *testing.T) {
input := `Before paragraph.
This should not appear in the output
After paragraph.
`
expected := `Before paragraph.
After paragraph.
`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
@@ -700,7 +720,7 @@ func TestAttributesAreStripped(t *testing.T) {
input := `Some text.
Test.`
expected := `Some text.
Test.`
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
}
@@ -709,7 +729,7 @@ func TestAttributesAreStripped(t *testing.T) {
func TestMathML(t *testing.T) {
input := ``
expected := ``
- output := Sanitize("http://example.org/", input)
+ output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output {
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
diff --git a/internal/storage/user.go b/internal/storage/user.go
index 97d69756..3a0a021a 100644
--- a/internal/storage/user.go
+++ b/internal/storage/user.go
@@ -97,7 +97,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules,
- always_open_external_links
+ always_open_external_links,
+ open_external_links_in_new_tab
`
tx, err := s.db.Begin()
@@ -142,6 +143,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.AlwaysOpenExternalLinks,
+ &user.OpenExternalLinksInNewTab,
)
if err != nil {
tx.Rollback()
@@ -207,9 +209,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
media_playback_rate=$26,
block_filter_entry_rules=$27,
keep_filter_entry_rules=$28,
- always_open_external_links=$29
+ always_open_external_links=$29,
+ open_external_links_in_new_tab=$30
WHERE
- id=$30
+ id=$31
`
_, err = s.db.Exec(
@@ -243,6 +246,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
user.AlwaysOpenExternalLinks,
+ user.OpenExternalLinksInNewTab,
user.ID,
)
if err != nil {
@@ -278,9 +282,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
media_playback_rate=$25,
block_filter_entry_rules=$26,
keep_filter_entry_rules=$27,
- always_open_external_links=$28
+ always_open_external_links=$28,
+ open_external_links_in_new_tab=$29
WHERE
- id=$29
+ id=$30
`
_, err := s.db.Exec(
@@ -313,6 +318,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.BlockFilterEntryRules,
user.KeepFilterEntryRules,
user.AlwaysOpenExternalLinks,
+ user.OpenExternalLinksInNewTab,
user.ID,
)
@@ -367,7 +373,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules,
- always_open_external_links
+ always_open_external_links,
+ open_external_links_in_new_tab
FROM
users
WHERE
@@ -409,7 +416,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules,
- always_open_external_links
+ always_open_external_links,
+ open_external_links_in_new_tab
FROM
users
WHERE
@@ -451,7 +459,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules,
- always_open_external_links
+ always_open_external_links,
+ open_external_links_in_new_tab
FROM
users
WHERE
@@ -500,7 +509,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
media_playback_rate,
u.block_filter_entry_rules,
u.keep_filter_entry_rules,
- u.always_open_external_links
+ u.always_open_external_links,
+ u.open_external_links_in_new_tab
FROM
users u
LEFT JOIN
@@ -544,6 +554,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.AlwaysOpenExternalLinks,
+ &user.OpenExternalLinksInNewTab,
)
if err == sql.ErrNoRows {
@@ -658,7 +669,8 @@ func (s *Storage) Users() (model.Users, error) {
media_playback_rate,
block_filter_entry_rules,
keep_filter_entry_rules,
- always_open_external_links
+ always_open_external_links,
+ open_external_links_in_new_tab
FROM
users
ORDER BY username ASC
@@ -703,6 +715,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.BlockFilterEntryRules,
&user.KeepFilterEntryRules,
&user.AlwaysOpenExternalLinks,
+ &user.OpenExternalLinksInNewTab,
)
if err != nil {
diff --git a/internal/template/templates/common/feed_list.html b/internal/template/templates/common/feed_list.html
index 3da5da9c..a72cfb1d 100644
--- a/internal/template/templates/common/feed_list.html
+++ b/internal/template/templates/common/feed_list.html
@@ -37,7 +37,7 @@
@@ -77,7 +77,7 @@
{{ t "form.feed.label.rewrite_rules" }}
-
+
{{ icon "external-link" }}
@@ -87,7 +87,7 @@
{{ t "form.feed.label.blocklist_rules" }}
-
+
{{ icon "external-link" }}
@@ -98,7 +98,7 @@
{{ t "form.feed.label.keeplist_rules" }}
-
+
{{ icon "external-link" }}
@@ -109,7 +109,7 @@
{{ t "form.feed.label.urlrewrite_rules" }}
-
+
{{ icon "external-link" }}
diff --git a/internal/template/templates/views/choose_subscription.html b/internal/template/templates/views/choose_subscription.html
index c8034a2b..e9597dcc 100644
--- a/internal/template/templates/views/choose_subscription.html
+++ b/internal/template/templates/views/choose_subscription.html
@@ -39,7 +39,7 @@
{{ range .subscriptions }}
{{ end }}
diff --git a/internal/template/templates/views/edit_feed.html b/internal/template/templates/views/edit_feed.html
index b55ad754..8921d18a 100644
--- a/internal/template/templates/views/edit_feed.html
+++ b/internal/template/templates/views/edit_feed.html
@@ -124,7 +124,7 @@
{{ t "form.feed.label.scraper_rules" }}
-
+
{{ icon "external-link" }}
@@ -135,7 +135,7 @@
{{ t "form.feed.label.rewrite_rules" }}
-
+
{{ icon "external-link" }}
@@ -145,7 +145,7 @@
{{ t "form.feed.label.blocklist_rules" }}
-
+
{{ icon "external-link" }}
@@ -156,7 +156,7 @@
{{ t "form.feed.label.keeplist_rules" }}
-
+
{{ icon "external-link" }}
@@ -167,7 +167,7 @@
{{ t "form.feed.label.urlrewrite_rules" }}
-
+
{{ icon "external-link" }}
@@ -205,7 +205,7 @@
{{ t "form.feed.label.ntfy_priority" }}
-
+
{{ icon "external-link" }}
@@ -226,7 +226,7 @@
{{ t "form.feed.label.pushover_priority" }}
-
+
{{ icon "external-link" }}
diff --git a/internal/template/templates/views/entry.html b/internal/template/templates/views/entry.html
index f5c33f0a..0ef05a20 100644
--- a/internal/template/templates/views/entry.html
+++ b/internal/template/templates/views/entry.html
@@ -4,7 +4,7 @@