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: Test.

` - 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 := `Example` 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 := `Example` expected := `Example` - 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 := `Example` expected := `Example` - 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 := `Example` expected := `Example` - 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 := `Image for post` expected := `Image for post` - 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: Test.

` - 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 := `
AB
CDE
` - 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.

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 := `x2` expected := `x2` - 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 }}
({{ .Type }}) - {{ .URL | safeURL }} + {{ .URL | safeURL }}
{{ 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 @@

- {{ .entry.Title }} + {{ .entry.Title }}

{{ if .user }}
@@ -53,7 +53,7 @@ {{ icon "share" }}{{ t "entry.shared_entry.label" }} + {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ icon "share" }}{{ t "entry.shared_entry.label" }}
  • diff --git a/internal/template/templates/views/feed_entries.html b/internal/template/templates/views/feed_entries.html index 47ebfaea..9271414c 100644 --- a/internal/template/templates/views/feed_entries.html +++ b/internal/template/templates/views/feed_entries.html @@ -3,7 +3,7 @@ {{ define "page_header"}}