mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
feat(ui): add user setting to control target="_blank"
on links
Rationale: Opening links in the current tab is the default browser behavior. Using `target="_blank"` on external links can lead to accessibility issues and override user preferences. It may also interfere with assistive technologies and expected browser behavior. To maintain backward compatibility, this option is enabled by default (`true`), which adds `target="_blank"` to links.
This commit is contained in:
parent
699deea72c
commit
c718eb039b
39 changed files with 345 additions and 259 deletions
118
client/model.go
118
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.
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Ζώνη Ώρας",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "समय क्षेत्र",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "タイムゾーン",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Часовой пояс",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Часовий пояс",
|
||||
|
|
|
@ -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": "时区",
|
||||
|
|
|
@ -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": "時區",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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 := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
|
||||
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 := `<img src="https://example.org/image.png" width="10" height="20">`
|
||||
expected := `<img src="https://example.org/image.png" width="10" height="20" loading="lazy">`
|
||||
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 := `<img src="https://example.org/image.png" width="1200" height="675">`
|
||||
expected := `<img src="https://example.org/image.png" loading="lazy">`
|
||||
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 := `<img src="https://example.org/image.png" width="10px" height="20px">`
|
||||
expected := `<img src="https://example.org/image.png" loading="lazy">`
|
||||
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 := `<img src="data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==" alt="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 := `<img src="data:image/gif;base64,test" alt="Example">`
|
||||
expected := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy">`
|
||||
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 := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" src="example-640w.jpg" alt="Example">`
|
||||
expected := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" src="http://example.org/example-640w.jpg" alt="Example" loading="lazy">`
|
||||
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 := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" alt="Example">`
|
||||
expected := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" alt="Example" loading="lazy">`
|
||||
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 := `<picture><source media="(min-width: 800px)" srcset="elva-800w.jpg"></picture>`
|
||||
expected := `<picture><source media="(min-width: 800px)" srcset="http://example.org/elva-800w.jpg"></picture>`
|
||||
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 := `<img alt="Image for post" class="t u v ef aj" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" width="2730" height="3407">`
|
||||
expected := `<img alt="Image for post" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" loading="lazy">`
|
||||
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 := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test" loading="lazy"/>.</p>`
|
||||
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 := `<table><tr><th>A</th><th colspan="2">B</th></tr><tr><td>C</td><td>D</td><td>E</td></tr></table>`
|
||||
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 <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>`
|
||||
expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy"/>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy"/>`
|
||||
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 <a href="//static.example.org/index.html">link is relative</a>.`
|
||||
expected := `This <a href="https://static.example.org/index.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a>.`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `This <a href="https://static.example.org/index.html" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">link is relative</a>.`
|
||||
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 := `<p>My invalid <z>tag</z>.</p>`
|
||||
expected := `<p>My invalid tag.</p>`
|
||||
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 := `<p>My valid <video src="videofile.webm" autoplay poster="posterimage.jpg">fallback</video>.</p>`
|
||||
expected := `<p>My valid <video src="http://example.org/videofile.webm" poster="http://example.org/posterimage.jpg" controls>fallback</video>.</p>`
|
||||
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 := `<p>My music <audio controls="controls"><source src="foo.wav" type="audio/wav"></audio>.</p>`
|
||||
expected := `<p>My music <audio controls><source src="http://example.org/foo.wav" type="audio/wav"></audio>.</p>`
|
||||
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 := `<p>My invalid <unknown>tag</unknown>.</p>`
|
||||
expected := `<p>My invalid tag.</p>`
|
||||
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 := `<p>My invalid <z>tag with some <em>valid</em> tag</z>.</p>`
|
||||
expected := `<p>My invalid tag with some <em>valid</em> tag.</p>`
|
||||
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 := `<iframe src="http://example.org/"></iframe>`
|
||||
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 := `<iframe src="https://www.youtube.com/"><p>test</p></iframe>`
|
||||
expected := `<iframe src="https://www.youtube.com/" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<p>This link is <a href="http://example.org/index.html">an anchor</a></p>`
|
||||
expected := `<p>This link is <a href="http://example.org/index.html" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">an anchor</a></p>`
|
||||
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 := `<p>This link is <a href="http://example.org/index.html">an anchor</a></p>`
|
||||
expected := `<p>This link is <a href="http://example.org/index.html" rel="noopener noreferrer" referrerpolicy="no-referrer">an anchor</a></p>`
|
||||
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 := `<p>This link is <a href="#some-anchor">an anchor</a></p>`
|
||||
expected := `<p>This link is <a href="#some-anchor">an anchor</a></p>`
|
||||
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 := `<p>This link is <a src="file:///etc/passwd">not valid</a></p>`
|
||||
expected := `<p>This link is not valid</p>`
|
||||
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 := `<p>This link is <a href="apt:some-package?channel=test">valid</a></p>`
|
||||
expected := `<p>This link is <a href="apt:some-package?channel=test" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="apt:some-package?channel=test" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W">valid</a></p>`
|
||||
expected := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="callto:12345679">valid</a></p>`
|
||||
expected := `<p>This link is <a href="callto:12345679" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="callto:12345679" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="feed://example.com/rss.xml">valid</a></p>`
|
||||
expected := `<p>This link is <a href="feed://example.com/rss.xml" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="feed://example.com/rss.xml" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="feed:https://example.com/rss.xml">valid</a></p>`
|
||||
expected = `<p>This link is <a href="feed:https://example.com/rss.xml" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="feed:https://example.com/rss.xml" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="geo:13.4125,103.8667">valid</a></p>`
|
||||
expected := `<p>This link is <a href="geo:13.4125,103.8667" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="geo:13.4125,103.8667" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="itms://itunes.com/apps/my-app-name">valid</a></p>`
|
||||
expected := `<p>This link is <a href="itms://itunes.com/apps/my-app-name" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="itms://itunes.com/apps/my-app-name" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="itms-apps://itunes.com/apps/my-app-name">valid</a></p>`
|
||||
expected = `<p>This link is <a href="itms-apps://itunes.com/apps/my-app-name" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="itms-apps://itunes.com/apps/my-app-name" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7">valid</a></p>`
|
||||
expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A">valid</a></p>`
|
||||
expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="news://news.server.example/*">valid</a></p>`
|
||||
expected := `<p>This link is <a href="news://news.server.example/*" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="news://news.server.example/*" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="news:example.group.this">valid</a></p>`
|
||||
expected = `<p>This link is <a href="news:example.group.this" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="news:example.group.this" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="nntp://news.server.example/example.group.this">valid</a></p>`
|
||||
expected = `<p>This link is <a href="nntp://news.server.example/example.group.this" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="nntp://news.server.example/example.group.this" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="rtmp://mycompany.com/vod/mp4:mycoolvideo.mov">valid</a></p>`
|
||||
expected := `<p>This link is <a href="rtmp://mycompany.com/vod/mp4:mycoolvideo.mov" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="rtmp://mycompany.com/vod/mp4:mycoolvideo.mov" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="sip:+1-212-555-1212:1234@gateway.com;user=phone">valid</a></p>`
|
||||
expected := `<p>This link is <a href="sip:+1-212-555-1212:1234@gateway.com;user=phone" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="sip:+1-212-555-1212:1234@gateway.com;user=phone" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent">valid</a></p>`
|
||||
expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="skype:echo123?call">valid</a></p>`
|
||||
expected := `<p>This link is <a href="skype:echo123?call" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="skype:echo123?call" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="spotify:track:2jCnn1QPQ3E8ExtLe6INsx">valid</a></p>`
|
||||
expected := `<p>This link is <a href="spotify:track:2jCnn1QPQ3E8ExtLe6INsx" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="spotify:track:2jCnn1QPQ3E8ExtLe6INsx" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="steam://settings/account">valid</a></p>`
|
||||
expected := `<p>This link is <a href="steam://settings/account" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="steam://settings/account" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="svn://example.org">valid</a></p>`
|
||||
expected := `<p>This link is <a href="svn://example.org" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="svn://example.org" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
}
|
||||
|
||||
input = `<p>This link is <a href="svn+ssh://example.org">valid</a></p>`
|
||||
expected = `<p>This link is <a href="svn+ssh://example.org" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output = Sanitize("http://example.org/", input)
|
||||
expected = `<p>This link is <a href="svn+ssh://example.org" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="tel:+1-201-555-0123">valid</a></p>`
|
||||
expected := `<p>This link is <a href="tel:+1-201-555-0123" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="tel:+1-201-555-0123" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="webcal://example.com/calendar.ics">valid</a></p>`
|
||||
expected := `<p>This link is <a href="webcal://example.com/calendar.ics" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="webcal://example.com/calendar.ics" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed">valid</a></p>`
|
||||
expected := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">valid</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||
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 := `<p>This image is not valid <img src="https://stats.wordpress.com/some-tracker"></p>`
|
||||
expected := `<p>This image is not valid </p>`
|
||||
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 := `<p>This link has trackers <a href="https://example.com/page?utm_source=newsletter">Test</a></p>`
|
||||
expected := `<p>This link has trackers <a href="https://example.com/page" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">Test</a></p>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
expected := `<p>This link has trackers <a href="https://example.com/page" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">Test</a></p>`
|
||||
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 := `<p>This image has trackers <img src="https://example.org/?id=123&utm_source=newsletter&utm_medium=email&fbclid=abc123"></p>`
|
||||
expected := `<p>This image has trackers <img src="https://example.org/?id=123" loading="lazy"></p>`
|
||||
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 := `<p><img src="https://tracker1.example.org/" height="1" width="1"> and <img src="https://tracker2.example.org/" height="1" width="1"/></p>`
|
||||
expected := `<p> and </p>`
|
||||
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 := `<pre>echo "test" > /etc/hosts</pre>`
|
||||
expected := `<pre>echo "test" > /etc/hosts</pre>`
|
||||
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 := `<td rowspan="<b>test</b>">test</td>`
|
||||
expected := `<td rowspan="<b>test</b>">test</td>`
|
||||
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 := `<iframe src="http://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="https://www.youtube.com/embed/test123"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="https://www.youtube.com/embed/test123?rel=0&controls=0"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="//www.youtube.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen"></iframe>`
|
||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="https://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0"></iframe>`
|
||||
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0&dnt=1" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||
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 := `<p>Before paragraph.</p><noscript>Inside <code>noscript</code> tag with an image: <img src="http://example.org/" alt="Test" loading="lazy"></noscript><p>After paragraph.</p>`
|
||||
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||
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 := `<p>Before paragraph.</p><script type="text/javascript">alert("1");</script><p>After paragraph.</p>`
|
||||
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||
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 := `<p>Before paragraph.</p><style>body { background-color: #ff0000; }</style><p>After paragraph.</p>`
|
||||
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||
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 := `<p>Before paragraph.</p><p hidden>This should <em>not</em> appear in the <strong>output</strong></p><p>After paragraph.</p>`
|
||||
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||
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 := `<p style="color: red;">Some text.<hr style="color: blue"/>Test.</p>`
|
||||
expected := `<p>Some text.<hr/>Test.</p>`
|
||||
|
||||
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 := `<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>x</mi><mn>2</mn></msup></math>`
|
||||
expected := `<math xmlns="http://www.w3.org/1998/Math/MathML"><msup><mi>x</mi><mn>2</mn></msup></math>`
|
||||
output := Sanitize("http://example.org/", input)
|
||||
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||
|
||||
if expected != output {
|
||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<div class="item-meta">
|
||||
<ul class="item-meta-info">
|
||||
<li class="item-meta-info-site-url" dir="auto">
|
||||
<a href="{{ .SiteURL | safeURL }}" title="{{ .SiteURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="{{ $.user.MarkReadOnView }}">{{ domain .SiteURL }}</a>
|
||||
<a href="{{ .SiteURL | safeURL }}" title="{{ .SiteURL }}" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }} rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="{{ $.user.MarkReadOnView }}">{{ domain .SiteURL }}</a>
|
||||
</li>
|
||||
<li class="item-meta-info-checked-at">
|
||||
{{ t "page.feeds.last_check" }} <time datetime="{{ isodate .CheckedAt }}" title="{{ isodate .CheckedAt }}">{{ elapsed $.user.Timezone .CheckedAt }}</time>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||
title="{{ t "entry.shared_entry.title" }}"
|
||||
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
|
||||
</li>
|
||||
<li class="item-meta-icons-delete">
|
||||
<button
|
||||
|
@ -71,7 +71,7 @@
|
|||
<li class="item-meta-icons-external-url">
|
||||
<a href="{{ .entry.URL | safeURL }}"
|
||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||
target="_blank"
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
||||
|
@ -81,7 +81,7 @@
|
|||
<a href="{{ .entry.CommentsURL | safeURL }}"
|
||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||
title="{{ t "entry.comments.title" }}"
|
||||
target="_blank"
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
data-comments-link="true">{{ icon "comment" }}<span class="icon-label">{{ t "entry.comments.label" }}</span></a>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
{{ t "form.feed.label.scraper_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#scraper-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#scraper-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -77,7 +77,7 @@
|
|||
{{ t "form.feed.label.rewrite_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#rewrite-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#rewrite-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -87,7 +87,7 @@
|
|||
{{ t "form.feed.label.blocklist_rules" }}
|
||||
</label>
|
||||
|
||||
<a href=" https://miniflux.app/docs/rules.html#feed-filtering-rules" target="_blank">
|
||||
<a href=" https://miniflux.app/docs/rules.html#feed-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@
|
|||
{{ t "form.feed.label.keeplist_rules" }}
|
||||
</label>
|
||||
|
||||
<a href=" https://miniflux.app/docs/rules.html#feed-filtering-rules" target="_blank">
|
||||
<a href=" https://miniflux.app/docs/rules.html#feed-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -109,7 +109,7 @@
|
|||
{{ t "form.feed.label.urlrewrite_rules" }}
|
||||
</label>
|
||||
|
||||
<a href=" https://miniflux.app/docs/rules.html#rewriteurl-rules" target="_blank">
|
||||
<a href=" https://miniflux.app/docs/rules.html#rewriteurl-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
{{ range .subscriptions }}
|
||||
<div class="radio-group">
|
||||
<label title="{{ .URL | safeURL }}"><input type="radio" name="url" value="{{ .URL | safeURL }}"> {{ .Title }}</label> ({{ .Type }})
|
||||
<small title="Type = {{ .Type }}"><a href="{{ .URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a></small>
|
||||
<small title="Type = {{ .Type }}"><a href="{{ .URL | safeURL }}" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }} rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a></small>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
{{ t "form.feed.label.scraper_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#scraper-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#scraper-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -135,7 +135,7 @@
|
|||
{{ t "form.feed.label.rewrite_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#rewrite-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#rewrite-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -145,7 +145,7 @@
|
|||
{{ t "form.feed.label.blocklist_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#feed-filtering-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#feed-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -156,7 +156,7 @@
|
|||
{{ t "form.feed.label.keeplist_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#feed-filtering-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#feed-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -167,7 +167,7 @@
|
|||
{{ t "form.feed.label.urlrewrite_rules" }}
|
||||
</label>
|
||||
|
||||
<a href="https://miniflux.app/docs/rules.html#rewriteurl-rules" target="_blank">
|
||||
<a href="https://miniflux.app/docs/rules.html#rewriteurl-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -205,7 +205,7 @@
|
|||
{{ t "form.feed.label.ntfy_priority" }}
|
||||
</label>
|
||||
|
||||
<a href="https://docs.ntfy.sh/publish/#message-priority" target="_blank">
|
||||
<a href="https://docs.ntfy.sh/publish/#message-priority" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -226,7 +226,7 @@
|
|||
{{ t "form.feed.label.pushover_priority" }}
|
||||
</label>
|
||||
|
||||
<a href="https://pushover.net/api#priority" target="_blank">
|
||||
<a href="https://pushover.net/api#priority" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
|
||||
<header class="entry-header">
|
||||
<h1 id="page-header-title" dir="auto">
|
||||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||||
<a href="{{ .entry.URL | safeURL }}" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }} rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||||
</h1>
|
||||
{{ if .user }}
|
||||
<div class="entry-actions">
|
||||
|
@ -53,7 +53,7 @@
|
|||
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
||||
title="{{ t "entry.shared_entry.title" }}"
|
||||
data-share-status="shared"
|
||||
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ icon "share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
|
@ -78,7 +78,7 @@
|
|||
<li>
|
||||
<a href="{{ .entry.URL | safeURL }}"
|
||||
class="page-link"
|
||||
target="_blank"
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<a href="{{ .entry.CommentsURL | safeURL }}"
|
||||
class="page-link"
|
||||
title="{{ t "entry.comments.title" }}"
|
||||
target="_blank"
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
data-comments-link="true"
|
||||
|
@ -267,7 +267,7 @@
|
|||
{{ end }}
|
||||
|
||||
<div class="entry-enclosure-download">
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }} rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||||
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{{ define "page_header"}}
|
||||
<section class="page-header" aria-labelledby="page-header-title">
|
||||
<h1 id="page-header-title" dir="auto">
|
||||
<a href="{{ .feed.SiteURL | safeURL }}" title="{{ .feed.SiteURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="{{ .user.MarkReadOnView }}">{{ .feed.Title }}</a>
|
||||
<a href="{{ .feed.SiteURL | safeURL }}" title="{{ .feed.SiteURL }}" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }} rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="{{ .user.MarkReadOnView }}">{{ .feed.Title }}</a>
|
||||
<span aria-hidden="true">({{ .total }})</span>
|
||||
</h1>
|
||||
<span class="sr-only">
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<input type="url" name="apprise_url" id="form-apprise-url" value="{{ .form.AppriseURL }}" placeholder="http://apprise:8080" spellcheck="false">
|
||||
|
||||
<label for="form-apprise-services-urls">{{ t "form.integration.apprise_services_url" }}
|
||||
<a href="https://github.com/caronc/apprise/wiki" target="_blank">
|
||||
<a href="https://github.com/caronc/apprise/wiki" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</label>
|
||||
|
@ -514,7 +514,7 @@
|
|||
<label for="form-readwise-api-key">{{ t "form.integration.readwise_api_key" }}</label>
|
||||
<input type="text" name="readwise_api_key" id="form-readwise-api-key" value="{{ .form.ReadwiseAPIKey }}" spellcheck="false">
|
||||
|
||||
<p><a href="https://readwise.io/access_token" target="_blank">{{ t "form.integration.readwise_api_key_link" }}</a></p>
|
||||
<p><a href="https://readwise.io/access_token" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ t "form.integration.readwise_api_key_link" }}</a></p>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
<div class="form-label-row">
|
||||
<label for="form-display-mode">{{ t "form.prefs.label.display_mode" }}</label>
|
||||
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/Manifest/display" target="_blank">
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/Manifest/display" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -209,6 +209,8 @@
|
|||
|
||||
<label><input type="checkbox" name="always_open_external_links" value="1" {{ if .form.AlwaysOpenExternalLinks }}checked{{ end }}> {{ t "form.prefs.label.always_open_external_links" }}</label>
|
||||
|
||||
<label><input type="checkbox" name="open_external_links_in_new_tab" value="1" {{ if .form.OpenExternalLinksInNewTab }}checked{{ end }}> {{ t "form.prefs.label.open_external_links_in_new_tab" }}</label>
|
||||
|
||||
<label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
|
||||
<textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
|
||||
|
||||
|
@ -231,7 +233,7 @@
|
|||
{{ t "form.feed.label.blocklist_rules" }}
|
||||
</label>
|
||||
|
||||
<a href=" https://miniflux.app/docs/rules.html#global-filtering-rules" target="_blank">
|
||||
<a href=" https://miniflux.app/docs/rules.html#global-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -241,7 +243,7 @@
|
|||
<label for="form-keeplist-rules">
|
||||
{{ t "form.feed.label.keeplist_rules" }}
|
||||
</label>
|
||||
<a href=" https://miniflux.app/docs/rules.html#global-filtering-rules" target="_blank">
|
||||
<a href=" https://miniflux.app/docs/rules.html#global-filtering-rules" {{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>
|
||||
{{ icon "external-link" }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
{{ if .ShareCode }}
|
||||
<a href="{{ route "sharedEntry" "shareCode" .ShareCode }}"
|
||||
title="{{ t "entry.shared_entry.title" }}"
|
||||
target="_blank">{{ icon "share" }}</a>
|
||||
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ icon "share" }}</a>
|
||||
{{ end }}
|
||||
</h2>
|
||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
||||
|
|
|
@ -48,11 +48,12 @@ type SettingsForm struct {
|
|||
CategoriesSortingOrder string
|
||||
MarkReadOnView bool
|
||||
// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
|
||||
MarkReadBehavior MarkReadBehavior
|
||||
MediaPlaybackRate float64
|
||||
BlockFilterEntryRules string
|
||||
KeepFilterEntryRules string
|
||||
AlwaysOpenExternalLinks bool
|
||||
MarkReadBehavior MarkReadBehavior
|
||||
MediaPlaybackRate float64
|
||||
BlockFilterEntryRules string
|
||||
KeepFilterEntryRules string
|
||||
AlwaysOpenExternalLinks bool
|
||||
OpenExternalLinksInNewTab bool
|
||||
}
|
||||
|
||||
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
|
||||
|
@ -116,6 +117,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
|||
user.BlockFilterEntryRules = s.BlockFilterEntryRules
|
||||
user.KeepFilterEntryRules = s.KeepFilterEntryRules
|
||||
user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks
|
||||
user.OpenExternalLinksInNewTab = s.OpenExternalLinksInNewTab
|
||||
|
||||
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
|
||||
user.MarkReadOnView = MarkReadOnView
|
||||
|
@ -181,32 +183,33 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
|
|||
mediaPlaybackRate = 1
|
||||
}
|
||||
return &SettingsForm{
|
||||
Username: r.FormValue("username"),
|
||||
Password: r.FormValue("password"),
|
||||
Confirmation: r.FormValue("confirmation"),
|
||||
Theme: r.FormValue("theme"),
|
||||
Language: r.FormValue("language"),
|
||||
Timezone: r.FormValue("timezone"),
|
||||
EntryDirection: r.FormValue("entry_direction"),
|
||||
EntryOrder: r.FormValue("entry_order"),
|
||||
EntriesPerPage: int(entriesPerPage),
|
||||
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
||||
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
||||
CustomCSS: r.FormValue("custom_css"),
|
||||
CustomJS: r.FormValue("custom_js"),
|
||||
ExternalFontHosts: r.FormValue("external_font_hosts"),
|
||||
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
||||
GestureNav: r.FormValue("gesture_nav"),
|
||||
DisplayMode: r.FormValue("display_mode"),
|
||||
DefaultReadingSpeed: int(defaultReadingSpeed),
|
||||
CJKReadingSpeed: int(cjkReadingSpeed),
|
||||
DefaultHomePage: r.FormValue("default_home_page"),
|
||||
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
||||
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
||||
MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")),
|
||||
MediaPlaybackRate: mediaPlaybackRate,
|
||||
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
||||
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
||||
AlwaysOpenExternalLinks: r.FormValue("always_open_external_links") == "1",
|
||||
Username: r.FormValue("username"),
|
||||
Password: r.FormValue("password"),
|
||||
Confirmation: r.FormValue("confirmation"),
|
||||
Theme: r.FormValue("theme"),
|
||||
Language: r.FormValue("language"),
|
||||
Timezone: r.FormValue("timezone"),
|
||||
EntryDirection: r.FormValue("entry_direction"),
|
||||
EntryOrder: r.FormValue("entry_order"),
|
||||
EntriesPerPage: int(entriesPerPage),
|
||||
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
||||
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
||||
CustomCSS: r.FormValue("custom_css"),
|
||||
CustomJS: r.FormValue("custom_js"),
|
||||
ExternalFontHosts: r.FormValue("external_font_hosts"),
|
||||
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
||||
GestureNav: r.FormValue("gesture_nav"),
|
||||
DisplayMode: r.FormValue("display_mode"),
|
||||
DefaultReadingSpeed: int(defaultReadingSpeed),
|
||||
CJKReadingSpeed: int(cjkReadingSpeed),
|
||||
DefaultHomePage: r.FormValue("default_home_page"),
|
||||
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
||||
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
||||
MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")),
|
||||
MediaPlaybackRate: mediaPlaybackRate,
|
||||
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
||||
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
||||
AlwaysOpenExternalLinks: r.FormValue("always_open_external_links") == "1",
|
||||
OpenExternalLinksInNewTab: r.FormValue("open_external_links_in_new_tab") == "1",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,30 +23,31 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
settingsForm := form.SettingsForm{
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
EntryDirection: user.EntryDirection,
|
||||
EntryOrder: user.EntryOrder,
|
||||
EntriesPerPage: user.EntriesPerPage,
|
||||
KeyboardShortcuts: user.KeyboardShortcuts,
|
||||
ShowReadingTime: user.ShowReadingTime,
|
||||
CustomCSS: user.Stylesheet,
|
||||
CustomJS: user.CustomJS,
|
||||
ExternalFontHosts: user.ExternalFontHosts,
|
||||
EntrySwipe: user.EntrySwipe,
|
||||
GestureNav: user.GestureNav,
|
||||
DisplayMode: user.DisplayMode,
|
||||
DefaultReadingSpeed: user.DefaultReadingSpeed,
|
||||
CJKReadingSpeed: user.CJKReadingSpeed,
|
||||
DefaultHomePage: user.DefaultHomePage,
|
||||
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
||||
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
|
||||
MediaPlaybackRate: user.MediaPlaybackRate,
|
||||
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
||||
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
||||
AlwaysOpenExternalLinks: user.AlwaysOpenExternalLinks,
|
||||
Username: user.Username,
|
||||
Theme: user.Theme,
|
||||
Language: user.Language,
|
||||
Timezone: user.Timezone,
|
||||
EntryDirection: user.EntryDirection,
|
||||
EntryOrder: user.EntryOrder,
|
||||
EntriesPerPage: user.EntriesPerPage,
|
||||
KeyboardShortcuts: user.KeyboardShortcuts,
|
||||
ShowReadingTime: user.ShowReadingTime,
|
||||
CustomCSS: user.Stylesheet,
|
||||
CustomJS: user.CustomJS,
|
||||
ExternalFontHosts: user.ExternalFontHosts,
|
||||
EntrySwipe: user.EntrySwipe,
|
||||
GestureNav: user.GestureNav,
|
||||
DisplayMode: user.DisplayMode,
|
||||
DefaultReadingSpeed: user.DefaultReadingSpeed,
|
||||
CJKReadingSpeed: user.CJKReadingSpeed,
|
||||
DefaultHomePage: user.DefaultHomePage,
|
||||
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
||||
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
|
||||
MediaPlaybackRate: user.MediaPlaybackRate,
|
||||
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
||||
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
||||
AlwaysOpenExternalLinks: user.AlwaysOpenExternalLinks,
|
||||
OpenExternalLinksInNewTab: user.OpenExternalLinksInNewTab,
|
||||
}
|
||||
|
||||
timezones, err := h.store.Timezones()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue