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
8db637cb39
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.
|
// User represents a user in the system.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
EntryDirection string `json:"entry_sorting_direction"`
|
EntryDirection string `json:"entry_sorting_direction"`
|
||||||
EntryOrder string `json:"entry_sorting_order"`
|
EntryOrder string `json:"entry_sorting_order"`
|
||||||
Stylesheet string `json:"stylesheet"`
|
Stylesheet string `json:"stylesheet"`
|
||||||
CustomJS string `json:"custom_js"`
|
CustomJS string `json:"custom_js"`
|
||||||
GoogleID string `json:"google_id"`
|
GoogleID string `json:"google_id"`
|
||||||
OpenIDConnectID string `json:"openid_connect_id"`
|
OpenIDConnectID string `json:"openid_connect_id"`
|
||||||
EntriesPerPage int `json:"entries_per_page"`
|
EntriesPerPage int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime bool `json:"show_reading_time"`
|
ShowReadingTime bool `json:"show_reading_time"`
|
||||||
EntrySwipe bool `json:"entry_swipe"`
|
EntrySwipe bool `json:"entry_swipe"`
|
||||||
GestureNav string `json:"gesture_nav"`
|
GestureNav string `json:"gesture_nav"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
DisplayMode string `json:"display_mode"`
|
DisplayMode string `json:"display_mode"`
|
||||||
DefaultReadingSpeed int `json:"default_reading_speed"`
|
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||||
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||||
DefaultHomePage string `json:"default_home_page"`
|
DefaultHomePage string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||||
ExternalFontHosts string `json:"external_font_hosts"`
|
ExternalFontHosts string `json:"external_font_hosts"`
|
||||||
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
|
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
|
||||||
|
OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) String() string {
|
func (u User) String() string {
|
||||||
|
@ -64,34 +65,35 @@ type UserCreationRequest struct {
|
||||||
|
|
||||||
// UserModificationRequest represents the request to update a user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
type UserModificationRequest struct {
|
type UserModificationRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Language *string `json:"language"`
|
Language *string `json:"language"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
EntryOrder *string `json:"entry_sorting_order"`
|
EntryOrder *string `json:"entry_sorting_order"`
|
||||||
Stylesheet *string `json:"stylesheet"`
|
Stylesheet *string `json:"stylesheet"`
|
||||||
CustomJS *string `json:"custom_js"`
|
CustomJS *string `json:"custom_js"`
|
||||||
GoogleID *string `json:"google_id"`
|
GoogleID *string `json:"google_id"`
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
GestureNav *string `json:"gesture_nav"`
|
GestureNav *string `json:"gesture_nav"`
|
||||||
DisplayMode *string `json:"display_mode"`
|
DisplayMode *string `json:"display_mode"`
|
||||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||||
DefaultHomePage *string `json:"default_home_page"`
|
DefaultHomePage *string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||||
ExternalFontHosts *string `json:"external_font_hosts"`
|
ExternalFontHosts *string `json:"external_font_hosts"`
|
||||||
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
|
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
|
||||||
|
OpenExternalLinksInNewTab *bool `json:"open_external_links_in_new_tab"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users represents a list of users.
|
// Users represents a list of users.
|
||||||
|
|
|
@ -1086,4 +1086,8 @@ var migrations = []func(tx *sql.Tx, driver string) error{
|
||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return
|
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": "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.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.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.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
|
||||||
"form.prefs.label.theme": "Thema",
|
"form.prefs.label.theme": "Thema",
|
||||||
"form.prefs.label.timezone": "Zeitzone",
|
"form.prefs.label.timezone": "Zeitzone",
|
||||||
|
|
|
@ -356,6 +356,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Σήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή. Για ήχο/βίντεο, σήμανση ως αναγνωσμένου στο 90%% ολοκλήρωσης",
|
"form.prefs.label.mark_read_on_view_or_media_completion": "Σήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή. Για ήχο/βίντεο, σήμανση ως αναγνωσμένου στο 90%% ολοκλήρωσης",
|
||||||
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
|
"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.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
|
||||||
"form.prefs.label.theme": "Θέμα",
|
"form.prefs.label.theme": "Θέμα",
|
||||||
"form.prefs.label.timezone": "Ζώνη Ώρας",
|
"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": "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.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.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.show_reading_time": "Show estimated reading time for entries",
|
||||||
"form.prefs.label.theme": "Theme",
|
"form.prefs.label.theme": "Theme",
|
||||||
"form.prefs.label.timezone": "Timezone",
|
"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": "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.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.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.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.timezone": "Zona horaria",
|
"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": "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.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.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.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
|
||||||
"form.prefs.label.theme": "Teema",
|
"form.prefs.label.theme": "Teema",
|
||||||
"form.prefs.label.timezone": "Aikavyöhyke",
|
"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": "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.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.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.show_reading_time": "Afficher le temps de lecture estimé des articles",
|
||||||
"form.prefs.label.theme": "Thème",
|
"form.prefs.label.theme": "Thème",
|
||||||
"form.prefs.label.timezone": "Fuseau horaire",
|
"form.prefs.label.timezone": "Fuseau horaire",
|
||||||
|
|
|
@ -356,6 +356,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
|
"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.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.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
||||||
|
"form.prefs.label.open_external_links_in_new_tab": "बाहरी लिंक को एक नए टैब में खोलें (लिंक में target=\"_blank\" जोड़ता है)",
|
||||||
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
|
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
|
||||||
"form.prefs.label.theme": "थीम",
|
"form.prefs.label.theme": "थीम",
|
||||||
"form.prefs.label.timezone": "समय क्षेत्र",
|
"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": "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.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.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.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.timezone": "Zona Waktu",
|
"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": "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.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.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.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.timezone": "Fuso orario",
|
"form.prefs.label.timezone": "Fuso orario",
|
||||||
|
|
|
@ -353,6 +353,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
|
"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.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.media_playback_rate": "オーディオ/ビデオの再生速度",
|
||||||
|
"form.prefs.label.open_external_links_in_new_tab": "外部リンクを新しいタブで開く(リンクに target=\"_blank\" を追加)",
|
||||||
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
|
||||||
"form.prefs.label.theme": "テーマ",
|
"form.prefs.label.theme": "テーマ",
|
||||||
"form.prefs.label.timezone": "タイムゾーン",
|
"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": "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.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.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.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.theme": "Chú-tôe",
|
||||||
"form.prefs.label.timezone": "Sî-khu",
|
"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": "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.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.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.show_reading_time": "Toon geschatte leestijd van artikelen",
|
||||||
"form.prefs.label.theme": "Thema",
|
"form.prefs.label.theme": "Thema",
|
||||||
"form.prefs.label.timezone": "Tijdzone",
|
"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": "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.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.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.show_reading_time": "Pokaż szacowany czas czytania wpisów",
|
||||||
"form.prefs.label.theme": "Wygląd",
|
"form.prefs.label.theme": "Wygląd",
|
||||||
"form.prefs.label.timezone": "Strefa czasowa",
|
"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": "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.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.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.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.timezone": "Fuso horário",
|
"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": "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.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.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.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
|
||||||
"form.prefs.label.theme": "Temă",
|
"form.prefs.label.theme": "Temă",
|
||||||
"form.prefs.label.timezone": "Fus orar",
|
"form.prefs.label.timezone": "Fus orar",
|
||||||
|
|
|
@ -359,6 +359,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения",
|
"form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения",
|
||||||
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
|
"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.show_reading_time": "Показать примерное время чтения статей",
|
||||||
"form.prefs.label.theme": "Тема",
|
"form.prefs.label.theme": "Тема",
|
||||||
"form.prefs.label.timezone": "Часовой пояс",
|
"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": "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.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.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.show_reading_time": "Makaleler için tahmini okuma süresini göster",
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.timezone": "Saat Dilimi",
|
"form.prefs.label.timezone": "Saat Dilimi",
|
||||||
|
|
|
@ -359,6 +359,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
|
"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.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.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
||||||
|
"form.prefs.label.open_external_links_in_new_tab": "Відкривати зовнішні посилання у новій вкладці (додає target=\"_blank\" до посилань)",
|
||||||
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
|
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
|
||||||
"form.prefs.label.theme": "Тема",
|
"form.prefs.label.theme": "Тема",
|
||||||
"form.prefs.label.timezone": "Часовий пояс",
|
"form.prefs.label.timezone": "Часовий пояс",
|
||||||
|
|
|
@ -353,6 +353,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
|
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
|
||||||
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
|
"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.show_reading_time": "显示文章的预计阅读时间",
|
||||||
"form.prefs.label.theme": "主题",
|
"form.prefs.label.theme": "主题",
|
||||||
"form.prefs.label.timezone": "时区",
|
"form.prefs.label.timezone": "时区",
|
||||||
|
|
|
@ -353,6 +353,7 @@
|
||||||
"form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀",
|
"form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記",
|
"form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記",
|
||||||
"form.prefs.label.media_playback_rate": "音訊/視訊播放速度",
|
"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.show_reading_time": "顯示文章的預計閱讀時間",
|
||||||
"form.prefs.label.theme": "主題",
|
"form.prefs.label.theme": "主題",
|
||||||
"form.prefs.label.timezone": "時區",
|
"form.prefs.label.timezone": "時區",
|
||||||
|
|
|
@ -42,6 +42,7 @@ type User struct {
|
||||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||||
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
|
AlwaysOpenExternalLinks bool `json:"always_open_external_links"`
|
||||||
|
OpenExternalLinksInNewTab bool `json:"open_external_links_in_new_tab"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCreationRequest represents the request to create a user.
|
// UserCreationRequest represents the request to create a user.
|
||||||
|
@ -84,6 +85,7 @@ type UserModificationRequest struct {
|
||||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||||
AlwaysOpenExternalLinks *bool `json:"always_open_external_links"`
|
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.
|
// Patch updates the User object with the modification request.
|
||||||
|
@ -203,6 +205,10 @@ func (u *UserModificationRequest) Patch(user *User) {
|
||||||
if u.AlwaysOpenExternalLinks != nil {
|
if u.AlwaysOpenExternalLinks != nil {
|
||||||
user.AlwaysOpenExternalLinks = *u.AlwaysOpenExternalLinks
|
user.AlwaysOpenExternalLinks = *u.AlwaysOpenExternalLinks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.OpenExternalLinksInNewTab != nil {
|
||||||
|
user.OpenExternalLinksInNewTab = *u.OpenExternalLinksInNewTab
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseTimezone converts last login date to the given timezone.
|
// 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.
|
// 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)
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,14 +112,23 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sanitize returns safe HTML.
|
type SanitizerOptions struct {
|
||||||
func Sanitize(baseURL, input string) string {
|
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 buffer strings.Builder
|
||||||
var tagStack []string
|
var tagStack []string
|
||||||
var parentTag string
|
var parentTag string
|
||||||
var blockedStack []string
|
var blockedStack []string
|
||||||
|
|
||||||
tokenizer := html.NewTokenizer(strings.NewReader(input))
|
tokenizer := html.NewTokenizer(strings.NewReader(rawHTML))
|
||||||
for {
|
for {
|
||||||
if tokenizer.Next() == html.ErrorToken {
|
if tokenizer.Next() == html.ErrorToken {
|
||||||
err := tokenizer.Err()
|
err := tokenizer.Err()
|
||||||
|
@ -166,7 +175,7 @@ func Sanitize(baseURL, input string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(blockedStack) == 0 && isValidTag(tagName) {
|
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 hasRequiredAttributes(tagName, attrNames) {
|
||||||
if len(attrNames) > 0 {
|
if len(attrNames) > 0 {
|
||||||
// Rewrite the start tag with allowed attributes.
|
// Rewrite the start tag with allowed attributes.
|
||||||
|
@ -194,7 +203,7 @@ func Sanitize(baseURL, input string) string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(blockedStack) == 0 && isValidTag(tagName) {
|
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 hasRequiredAttributes(tagName, attrNames) {
|
||||||
if len(attrNames) > 0 {
|
if len(attrNames) > 0 {
|
||||||
buffer.WriteString("<" + tagName + " " + htmlAttributes + "/>")
|
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 htmlAttrs, attrNames []string
|
||||||
var err error
|
var err error
|
||||||
var isImageLargerThanLayout bool
|
var isImageLargerThanLayout bool
|
||||||
|
@ -269,7 +278,7 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isAnchorLink {
|
if !isAnchorLink {
|
||||||
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName)
|
extraAttrNames, extraHTMLAttributes := getExtraAttributes(tagName, sanitizerOptions)
|
||||||
if len(extraAttrNames) > 0 {
|
if len(extraAttrNames) > 0 {
|
||||||
attrNames = append(attrNames, extraAttrNames...)
|
attrNames = append(attrNames, extraAttrNames...)
|
||||||
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
|
htmlAttrs = append(htmlAttrs, extraHTMLAttributes...)
|
||||||
|
@ -279,10 +288,16 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
|
||||||
return attrNames, strings.Join(htmlAttrs, " ")
|
return attrNames, strings.Join(htmlAttrs, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExtraAttributes(tagName string) ([]string, []string) {
|
func getExtraAttributes(tagName string, sanitizerOptions *SanitizerOptions) ([]string, []string) {
|
||||||
switch tagName {
|
switch tagName {
|
||||||
case "a":
|
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":
|
case "video", "audio":
|
||||||
return []string{"controls"}, []string{"controls"}
|
return []string{"controls"}, []string{"controls"}
|
||||||
case "iframe":
|
case "iframe":
|
||||||
|
|
|
@ -33,7 +33,7 @@ func BenchmarkSanitize(b *testing.B) {
|
||||||
}
|
}
|
||||||
for range b.N {
|
for range b.N {
|
||||||
for _, v := range testCases {
|
for _, v := range testCases {
|
||||||
Sanitize(v[0], v[1])
|
SanitizeHTMLWithDefaultOptions(v[0], v[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ func FuzzSanitizer(f *testing.F) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
out := Sanitize("", orig)
|
out := SanitizeHTMLWithDefaultOptions("", orig)
|
||||||
|
|
||||||
tok = html.NewTokenizer(strings.NewReader(out))
|
tok = html.NewTokenizer(strings.NewReader(out))
|
||||||
j := 0
|
j := 0
|
||||||
|
@ -62,7 +62,7 @@ func FuzzSanitizer(f *testing.F) {
|
||||||
|
|
||||||
func TestValidInput(t *testing.T) {
|
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>`
|
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 {
|
if input != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
||||||
|
@ -72,7 +72,7 @@ func TestValidInput(t *testing.T) {
|
||||||
func TestImgWithWidthAndHeightAttribute(t *testing.T) {
|
func TestImgWithWidthAndHeightAttribute(t *testing.T) {
|
||||||
input := `<img src="https://example.org/image.png" width="10" height="20">`
|
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">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -82,7 +82,7 @@ func TestImgWithWidthAndHeightAttribute(t *testing.T) {
|
||||||
func TestImgWithWidthAndHeightAttributeLargerThanMinifluxLayout(t *testing.T) {
|
func TestImgWithWidthAndHeightAttributeLargerThanMinifluxLayout(t *testing.T) {
|
||||||
input := `<img src="https://example.org/image.png" width="1200" height="675">`
|
input := `<img src="https://example.org/image.png" width="1200" height="675">`
|
||||||
expected := `<img src="https://example.org/image.png" loading="lazy">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -92,7 +92,7 @@ func TestImgWithWidthAndHeightAttributeLargerThanMinifluxLayout(t *testing.T) {
|
||||||
func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
|
func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
|
||||||
input := `<img src="https://example.org/image.png" width="10px" height="20px">`
|
input := `<img src="https://example.org/image.png" width="10px" height="20px">`
|
||||||
expected := `<img src="https://example.org/image.png" loading="lazy">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -102,7 +102,7 @@ func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
|
||||||
func TestImgWithTextDataURL(t *testing.T) {
|
func TestImgWithTextDataURL(t *testing.T) {
|
||||||
input := `<img src="data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==" alt="Example">`
|
input := `<img src="data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==" alt="Example">`
|
||||||
expected := ``
|
expected := ``
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -112,7 +112,7 @@ func TestImgWithTextDataURL(t *testing.T) {
|
||||||
func TestImgWithDataURL(t *testing.T) {
|
func TestImgWithDataURL(t *testing.T) {
|
||||||
input := `<img src="data:image/gif;base64,test" alt="Example">`
|
input := `<img src="data:image/gif;base64,test" alt="Example">`
|
||||||
expected := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -122,7 +122,7 @@ func TestImgWithDataURL(t *testing.T) {
|
||||||
func TestImgWithSrcsetAttribute(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">`
|
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">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -132,7 +132,7 @@ func TestImgWithSrcsetAttribute(t *testing.T) {
|
||||||
func TestImgWithSrcsetAndNoSrcAttribute(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">`
|
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">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -142,7 +142,7 @@ func TestImgWithSrcsetAndNoSrcAttribute(t *testing.T) {
|
||||||
func TestSourceWithSrcsetAndMedia(t *testing.T) {
|
func TestSourceWithSrcsetAndMedia(t *testing.T) {
|
||||||
input := `<picture><source media="(min-width: 800px)" srcset="elva-800w.jpg"></picture>`
|
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>`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -152,7 +152,7 @@ func TestSourceWithSrcsetAndMedia(t *testing.T) {
|
||||||
func TestMediumImgWithSrcset(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">`
|
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">`
|
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 {
|
if output != expected {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf(`Wrong output: %s`, output)
|
||||||
|
@ -161,7 +161,7 @@ func TestMediumImgWithSrcset(t *testing.T) {
|
||||||
|
|
||||||
func TestSelfClosingTags(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>`
|
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 {
|
if input != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
||||||
|
@ -170,7 +170,7 @@ func TestSelfClosingTags(t *testing.T) {
|
||||||
|
|
||||||
func TestTable(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>`
|
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 {
|
if input != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
||||||
|
@ -179,8 +179,8 @@ func TestTable(t *testing.T) {
|
||||||
|
|
||||||
func TestRelativeURL(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"/>`
|
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"/>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -189,8 +189,8 @@ func TestRelativeURL(t *testing.T) {
|
||||||
|
|
||||||
func TestProtocolRelativeURL(t *testing.T) {
|
func TestProtocolRelativeURL(t *testing.T) {
|
||||||
input := `This <a href="//static.example.org/index.html">link is relative</a>.`
|
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>.`
|
expected := `This <a href="https://static.example.org/index.html" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">link is relative</a>.`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -200,7 +200,7 @@ func TestProtocolRelativeURL(t *testing.T) {
|
||||||
func TestInvalidTag(t *testing.T) {
|
func TestInvalidTag(t *testing.T) {
|
||||||
input := `<p>My invalid <z>tag</z>.</p>`
|
input := `<p>My invalid <z>tag</z>.</p>`
|
||||||
expected := `<p>My invalid tag.</p>`
|
expected := `<p>My invalid tag.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -210,7 +210,7 @@ func TestInvalidTag(t *testing.T) {
|
||||||
func TestVideoTag(t *testing.T) {
|
func TestVideoTag(t *testing.T) {
|
||||||
input := `<p>My valid <video src="videofile.webm" autoplay poster="posterimage.jpg">fallback</video>.</p>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -220,7 +220,7 @@ func TestVideoTag(t *testing.T) {
|
||||||
func TestAudioAndSourceTag(t *testing.T) {
|
func TestAudioAndSourceTag(t *testing.T) {
|
||||||
input := `<p>My music <audio controls="controls"><source src="foo.wav" type="audio/wav"></audio>.</p>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -230,7 +230,7 @@ func TestAudioAndSourceTag(t *testing.T) {
|
||||||
func TestUnknownTag(t *testing.T) {
|
func TestUnknownTag(t *testing.T) {
|
||||||
input := `<p>My invalid <unknown>tag</unknown>.</p>`
|
input := `<p>My invalid <unknown>tag</unknown>.</p>`
|
||||||
expected := `<p>My invalid tag.</p>`
|
expected := `<p>My invalid tag.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -240,7 +240,7 @@ func TestUnknownTag(t *testing.T) {
|
||||||
func TestInvalidNestedTag(t *testing.T) {
|
func TestInvalidNestedTag(t *testing.T) {
|
||||||
input := `<p>My invalid <z>tag with some <em>valid</em> tag</z>.</p>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -250,7 +250,7 @@ func TestInvalidNestedTag(t *testing.T) {
|
||||||
func TestInvalidIFrame(t *testing.T) {
|
func TestInvalidIFrame(t *testing.T) {
|
||||||
input := `<iframe src="http://example.org/"></iframe>`
|
input := `<iframe src="http://example.org/"></iframe>`
|
||||||
expected := ``
|
expected := ``
|
||||||
output := Sanitize("http://example.com/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.com/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -260,7 +260,27 @@ func TestInvalidIFrame(t *testing.T) {
|
||||||
func TestIFrameWithChildElements(t *testing.T) {
|
func TestIFrameWithChildElements(t *testing.T) {
|
||||||
input := `<iframe src="https://www.youtube.com/"><p>test</p></iframe>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -270,7 +290,7 @@ func TestIFrameWithChildElements(t *testing.T) {
|
||||||
func TestAnchorLink(t *testing.T) {
|
func TestAnchorLink(t *testing.T) {
|
||||||
input := `<p>This link is <a href="#some-anchor">an anchor</a></p>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -280,7 +300,7 @@ func TestAnchorLink(t *testing.T) {
|
||||||
func TestInvalidURLScheme(t *testing.T) {
|
func TestInvalidURLScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a src="file:///etc/passwd">not valid</a></p>`
|
input := `<p>This link is <a src="file:///etc/passwd">not valid</a></p>`
|
||||||
expected := `<p>This link is not valid</p>`
|
expected := `<p>This link is not valid</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -289,8 +309,8 @@ func TestInvalidURLScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestAPTURIScheme(t *testing.T) {
|
func TestAPTURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="apt:some-package?channel=test">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="apt:some-package?channel=test" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -299,8 +319,8 @@ func TestAPTURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestBitcoinURIScheme(t *testing.T) {
|
func TestBitcoinURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -309,8 +329,8 @@ func TestBitcoinURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestCallToURIScheme(t *testing.T) {
|
func TestCallToURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="callto:12345679">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="callto:12345679" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -319,16 +339,16 @@ func TestCallToURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestFeedURIScheme(t *testing.T) {
|
func TestFeedURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="feed://example.com/rss.xml">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="feed://example.com/rss.xml" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
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>`
|
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 = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -337,8 +357,8 @@ func TestFeedURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestGeoURIScheme(t *testing.T) {
|
func TestGeoURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="geo:13.4125,103.8667">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="geo:13.4125,103.8667" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -347,16 +367,16 @@ func TestGeoURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestItunesURIScheme(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>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
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>`
|
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 = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -365,8 +385,8 @@ func TestItunesURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestMagnetURIScheme(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>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -375,8 +395,8 @@ func TestMagnetURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestMailtoURIScheme(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>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -385,24 +405,24 @@ func TestMailtoURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestNewsURIScheme(t *testing.T) {
|
func TestNewsURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="news://news.server.example/*">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="news://news.server.example/*" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
input = `<p>This link is <a href="news:example.group.this">valid</a></p>`
|
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>`
|
expected = `<p>This link is <a href="news:example.group.this" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
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>`
|
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 = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -411,8 +431,8 @@ func TestNewsURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestRTMPURIScheme(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>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -421,16 +441,16 @@ func TestRTMPURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestSIPURIScheme(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>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
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>`
|
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 = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -439,8 +459,8 @@ func TestSIPURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestSkypeURIScheme(t *testing.T) {
|
func TestSkypeURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="skype:echo123?call">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="skype:echo123?call" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -449,8 +469,8 @@ func TestSkypeURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestSpotifyURIScheme(t *testing.T) {
|
func TestSpotifyURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="spotify:track:2jCnn1QPQ3E8ExtLe6INsx">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="spotify:track:2jCnn1QPQ3E8ExtLe6INsx" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -459,8 +479,8 @@ func TestSpotifyURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestSteamURIScheme(t *testing.T) {
|
func TestSteamURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="steam://settings/account">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="steam://settings/account" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -469,16 +489,16 @@ func TestSteamURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestSubversionURIScheme(t *testing.T) {
|
func TestSubversionURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="svn://example.org">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="svn://example.org" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
input = `<p>This link is <a href="svn+ssh://example.org">valid</a></p>`
|
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>`
|
expected = `<p>This link is <a href="svn+ssh://example.org" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output = Sanitize("http://example.org/", input)
|
output = SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -487,8 +507,8 @@ func TestSubversionURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestTelURIScheme(t *testing.T) {
|
func TestTelURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="tel:+1-201-555-0123">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="tel:+1-201-555-0123" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -497,8 +517,8 @@ func TestTelURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestWebcalURIScheme(t *testing.T) {
|
func TestWebcalURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="webcal://example.com/calendar.ics">valid</a></p>`
|
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>`
|
expected := `<p>This link is <a href="webcal://example.com/calendar.ics" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -507,8 +527,8 @@ func TestWebcalURIScheme(t *testing.T) {
|
||||||
|
|
||||||
func TestXMPPURIScheme(t *testing.T) {
|
func TestXMPPURIScheme(t *testing.T) {
|
||||||
input := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed">valid</a></p>`
|
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>`
|
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 := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -518,7 +538,7 @@ func TestXMPPURIScheme(t *testing.T) {
|
||||||
func TestBlacklistedLink(t *testing.T) {
|
func TestBlacklistedLink(t *testing.T) {
|
||||||
input := `<p>This image is not valid <img src="https://stats.wordpress.com/some-tracker"></p>`
|
input := `<p>This image is not valid <img src="https://stats.wordpress.com/some-tracker"></p>`
|
||||||
expected := `<p>This image is not valid </p>`
|
expected := `<p>This image is not valid </p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -527,8 +547,8 @@ func TestBlacklistedLink(t *testing.T) {
|
||||||
|
|
||||||
func TestLinkWithTrackers(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>`
|
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>`
|
expected := `<p>This link has trackers <a href="https://example.com/page" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">Test</a></p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -538,7 +558,7 @@ func TestLinkWithTrackers(t *testing.T) {
|
||||||
func TestImageSrcWithTrackers(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>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -548,7 +568,7 @@ func TestImageSrcWithTrackers(t *testing.T) {
|
||||||
func TestPixelTracker(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>`
|
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>`
|
expected := `<p> and </p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -558,7 +578,7 @@ func TestPixelTracker(t *testing.T) {
|
||||||
func TestXmlEntities(t *testing.T) {
|
func TestXmlEntities(t *testing.T) {
|
||||||
input := `<pre>echo "test" > /etc/hosts</pre>`
|
input := `<pre>echo "test" > /etc/hosts</pre>`
|
||||||
expected := `<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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -568,7 +588,7 @@ func TestXmlEntities(t *testing.T) {
|
||||||
func TestEspaceAttributes(t *testing.T) {
|
func TestEspaceAttributes(t *testing.T) {
|
||||||
input := `<td rowspan="<b>test</b>">test</td>`
|
input := `<td rowspan="<b>test</b>">test</td>`
|
||||||
expected := `<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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -578,7 +598,7 @@ func TestEspaceAttributes(t *testing.T) {
|
||||||
func TestReplaceYoutubeURL(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>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -588,7 +608,7 @@ func TestReplaceYoutubeURL(t *testing.T) {
|
||||||
func TestReplaceSecureYoutubeURL(t *testing.T) {
|
func TestReplaceSecureYoutubeURL(t *testing.T) {
|
||||||
input := `<iframe src="https://www.youtube.com/embed/test123"></iframe>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -598,7 +618,7 @@ func TestReplaceSecureYoutubeURL(t *testing.T) {
|
||||||
func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
|
func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
|
||||||
input := `<iframe src="https://www.youtube.com/embed/test123?rel=0&controls=0"></iframe>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -608,7 +628,7 @@ func TestReplaceSecureYoutubeURLWithParameters(t *testing.T) {
|
||||||
func TestReplaceYoutubeURLAlreadyReplaced(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>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -618,7 +638,7 @@ func TestReplaceYoutubeURLAlreadyReplaced(t *testing.T) {
|
||||||
func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
|
func TestReplaceProtocolRelativeYoutubeURL(t *testing.T) {
|
||||||
input := `<iframe src="//www.youtube.com/embed/Bf2W84jrGqs" width="560" height="314" allowfullscreen="allowfullscreen"></iframe>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -649,7 +669,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
|
||||||
func TestReplaceIframeVimedoDNTURL(t *testing.T) {
|
func TestReplaceIframeVimedoDNTURL(t *testing.T) {
|
||||||
input := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0"></iframe>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -659,7 +679,7 @@ func TestReplaceIframeVimedoDNTURL(t *testing.T) {
|
||||||
func TestReplaceNoScript(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>`
|
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>`
|
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -669,7 +689,7 @@ func TestReplaceNoScript(t *testing.T) {
|
||||||
func TestReplaceScript(t *testing.T) {
|
func TestReplaceScript(t *testing.T) {
|
||||||
input := `<p>Before paragraph.</p><script type="text/javascript">alert("1");</script><p>After paragraph.</p>`
|
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>`
|
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -679,7 +699,7 @@ func TestReplaceScript(t *testing.T) {
|
||||||
func TestReplaceStyle(t *testing.T) {
|
func TestReplaceStyle(t *testing.T) {
|
||||||
input := `<p>Before paragraph.</p><style>body { background-color: #ff0000; }</style><p>After paragraph.</p>`
|
input := `<p>Before paragraph.</p><style>body { background-color: #ff0000; }</style><p>After paragraph.</p>`
|
||||||
expected := `<p>Before paragraph.</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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
|
@ -689,7 +709,7 @@ func TestReplaceStyle(t *testing.T) {
|
||||||
func TestHiddenParagraph(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>`
|
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>`
|
expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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>`
|
input := `<p style="color: red;">Some text.<hr style="color: blue"/>Test.</p>`
|
||||||
expected := `<p>Some text.<hr/>Test.</p>`
|
expected := `<p>Some text.<hr/>Test.</p>`
|
||||||
|
|
||||||
output := Sanitize("http://example.org/", input)
|
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input)
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
||||||
}
|
}
|
||||||
|
@ -709,7 +729,7 @@ func TestAttributesAreStripped(t *testing.T) {
|
||||||
func TestMathML(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>`
|
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>`
|
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 {
|
if expected != output {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, 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,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_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()
|
tx, err := s.db.Begin()
|
||||||
|
@ -142,6 +143,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
|
||||||
&user.BlockFilterEntryRules,
|
&user.BlockFilterEntryRules,
|
||||||
&user.KeepFilterEntryRules,
|
&user.KeepFilterEntryRules,
|
||||||
&user.AlwaysOpenExternalLinks,
|
&user.AlwaysOpenExternalLinks,
|
||||||
|
&user.OpenExternalLinksInNewTab,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
@ -207,9 +209,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
media_playback_rate=$26,
|
media_playback_rate=$26,
|
||||||
block_filter_entry_rules=$27,
|
block_filter_entry_rules=$27,
|
||||||
keep_filter_entry_rules=$28,
|
keep_filter_entry_rules=$28,
|
||||||
always_open_external_links=$29
|
always_open_external_links=$29,
|
||||||
|
open_external_links_in_new_tab=$30
|
||||||
WHERE
|
WHERE
|
||||||
id=$30
|
id=$31
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
|
@ -243,6 +246,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.BlockFilterEntryRules,
|
user.BlockFilterEntryRules,
|
||||||
user.KeepFilterEntryRules,
|
user.KeepFilterEntryRules,
|
||||||
user.AlwaysOpenExternalLinks,
|
user.AlwaysOpenExternalLinks,
|
||||||
|
user.OpenExternalLinksInNewTab,
|
||||||
user.ID,
|
user.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -278,9 +282,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
media_playback_rate=$25,
|
media_playback_rate=$25,
|
||||||
block_filter_entry_rules=$26,
|
block_filter_entry_rules=$26,
|
||||||
keep_filter_entry_rules=$27,
|
keep_filter_entry_rules=$27,
|
||||||
always_open_external_links=$28
|
always_open_external_links=$28,
|
||||||
|
open_external_links_in_new_tab=$29
|
||||||
WHERE
|
WHERE
|
||||||
id=$29
|
id=$30
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
|
@ -313,6 +318,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.BlockFilterEntryRules,
|
user.BlockFilterEntryRules,
|
||||||
user.KeepFilterEntryRules,
|
user.KeepFilterEntryRules,
|
||||||
user.AlwaysOpenExternalLinks,
|
user.AlwaysOpenExternalLinks,
|
||||||
|
user.OpenExternalLinksInNewTab,
|
||||||
user.ID,
|
user.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -367,7 +373,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules,
|
keep_filter_entry_rules,
|
||||||
always_open_external_links
|
always_open_external_links,
|
||||||
|
open_external_links_in_new_tab
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -409,7 +416,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules,
|
keep_filter_entry_rules,
|
||||||
always_open_external_links
|
always_open_external_links,
|
||||||
|
open_external_links_in_new_tab
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -451,7 +459,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules,
|
keep_filter_entry_rules,
|
||||||
always_open_external_links
|
always_open_external_links,
|
||||||
|
open_external_links_in_new_tab
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -500,7 +509,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
u.block_filter_entry_rules,
|
u.block_filter_entry_rules,
|
||||||
u.keep_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
|
FROM
|
||||||
users u
|
users u
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -544,6 +554,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
|
||||||
&user.BlockFilterEntryRules,
|
&user.BlockFilterEntryRules,
|
||||||
&user.KeepFilterEntryRules,
|
&user.KeepFilterEntryRules,
|
||||||
&user.AlwaysOpenExternalLinks,
|
&user.AlwaysOpenExternalLinks,
|
||||||
|
&user.OpenExternalLinksInNewTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
@ -658,7 +669,8 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
media_playback_rate,
|
media_playback_rate,
|
||||||
block_filter_entry_rules,
|
block_filter_entry_rules,
|
||||||
keep_filter_entry_rules,
|
keep_filter_entry_rules,
|
||||||
always_open_external_links
|
always_open_external_links,
|
||||||
|
open_external_links_in_new_tab
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
ORDER BY username ASC
|
ORDER BY username ASC
|
||||||
|
@ -703,6 +715,7 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
&user.BlockFilterEntryRules,
|
&user.BlockFilterEntryRules,
|
||||||
&user.KeepFilterEntryRules,
|
&user.KeepFilterEntryRules,
|
||||||
&user.AlwaysOpenExternalLinks,
|
&user.AlwaysOpenExternalLinks,
|
||||||
|
&user.OpenExternalLinksInNewTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<div class="item-meta">
|
<div class="item-meta">
|
||||||
<ul class="item-meta-info">
|
<ul class="item-meta-info">
|
||||||
<li class="item-meta-info-site-url" dir="auto">
|
<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>
|
||||||
<li class="item-meta-info-checked-at">
|
<li class="item-meta-info-checked-at">
|
||||||
{{ t "page.feeds.last_check" }} <time datetime="{{ isodate .CheckedAt }}" title="{{ isodate .CheckedAt }}">{{ elapsed $.user.Timezone .CheckedAt }}</time>
|
{{ 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 }}"
|
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
||||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||||
title="{{ t "entry.shared_entry.title" }}"
|
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>
|
||||||
<li class="item-meta-icons-delete">
|
<li class="item-meta-icons-delete">
|
||||||
<button
|
<button
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<li class="item-meta-icons-external-url">
|
<li class="item-meta-icons-external-url">
|
||||||
<a href="{{ .entry.URL | safeURL }}"
|
<a href="{{ .entry.URL | safeURL }}"
|
||||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||||
target="_blank"
|
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
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 }}"
|
<a href="{{ .entry.CommentsURL | safeURL }}"
|
||||||
aria-describedby="entry-title-{{ .entry.ID }}"
|
aria-describedby="entry-title-{{ .entry.ID }}"
|
||||||
title="{{ t "entry.comments.title" }}"
|
title="{{ t "entry.comments.title" }}"
|
||||||
target="_blank"
|
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
data-comments-link="true">{{ icon "comment" }}<span class="icon-label">{{ t "entry.comments.label" }}</span></a>
|
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" }}
|
{{ t "form.feed.label.scraper_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
{{ t "form.feed.label.rewrite_rules" }}
|
{{ t "form.feed.label.rewrite_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
{{ t "form.feed.label.blocklist_rules" }}
|
{{ t "form.feed.label.blocklist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
{{ t "form.feed.label.keeplist_rules" }}
|
{{ t "form.feed.label.keeplist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
{{ t "form.feed.label.urlrewrite_rules" }}
|
{{ t "form.feed.label.urlrewrite_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
{{ range .subscriptions }}
|
{{ range .subscriptions }}
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
<label title="{{ .URL | safeURL }}"><input type="radio" name="url" value="{{ .URL | safeURL }}"> {{ .Title }}</label> ({{ .Type }})
|
<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>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
{{ t "form.feed.label.scraper_rules" }}
|
{{ t "form.feed.label.scraper_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
{{ t "form.feed.label.rewrite_rules" }}
|
{{ t "form.feed.label.rewrite_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
{{ t "form.feed.label.blocklist_rules" }}
|
{{ t "form.feed.label.blocklist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
{{ t "form.feed.label.keeplist_rules" }}
|
{{ t "form.feed.label.keeplist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
{{ t "form.feed.label.urlrewrite_rules" }}
|
{{ t "form.feed.label.urlrewrite_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -205,7 +205,7 @@
|
||||||
{{ t "form.feed.label.ntfy_priority" }}
|
{{ t "form.feed.label.ntfy_priority" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -226,7 +226,7 @@
|
||||||
{{ t "form.feed.label.pushover_priority" }}
|
{{ t "form.feed.label.pushover_priority" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
|
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
|
||||||
<header class="entry-header">
|
<header class="entry-header">
|
||||||
<h1 id="page-header-title" dir="auto">
|
<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>
|
</h1>
|
||||||
{{ if .user }}
|
{{ if .user }}
|
||||||
<div class="entry-actions">
|
<div class="entry-actions">
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
<a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
|
||||||
title="{{ t "entry.shared_entry.title" }}"
|
title="{{ t "entry.shared_entry.title" }}"
|
||||||
data-share-status="shared"
|
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>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ .entry.URL | safeURL }}"
|
<a href="{{ .entry.URL | safeURL }}"
|
||||||
class="page-link"
|
class="page-link"
|
||||||
target="_blank"
|
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
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 }}"
|
<a href="{{ .entry.CommentsURL | safeURL }}"
|
||||||
class="page-link"
|
class="page-link"
|
||||||
title="{{ t "entry.comments.title" }}"
|
title="{{ t "entry.comments.title" }}"
|
||||||
target="_blank"
|
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
referrerpolicy="no-referrer"
|
referrerpolicy="no-referrer"
|
||||||
data-comments-link="true"
|
data-comments-link="true"
|
||||||
|
@ -267,7 +267,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div class="entry-enclosure-download">
|
<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>
|
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{{ define "page_header"}}
|
{{ define "page_header"}}
|
||||||
<section class="page-header" aria-labelledby="page-header-title">
|
<section class="page-header" aria-labelledby="page-header-title">
|
||||||
<h1 id="page-header-title" dir="auto">
|
<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>
|
<span aria-hidden="true">({{ .total }})</span>
|
||||||
</h1>
|
</h1>
|
||||||
<span class="sr-only">
|
<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">
|
<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" }}
|
<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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
|
@ -514,7 +514,7 @@
|
||||||
<label for="form-readwise-api-key">{{ t "form.integration.readwise_api_key" }}</label>
|
<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">
|
<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">
|
<div class="buttons">
|
||||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
<div class="form-label-row">
|
<div class="form-label-row">
|
||||||
<label for="form-display-mode">{{ t "form.prefs.label.display_mode" }}</label>
|
<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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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="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>
|
<label for="form-custom-css">{{t "form.prefs.label.custom_css" }}</label>
|
||||||
<textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
|
<textarea id="form-custom-css" name="custom_css" cols="40" rows="10" spellcheck="false">{{ .form.CustomCSS }}</textarea>
|
||||||
|
|
||||||
|
@ -231,7 +233,7 @@
|
||||||
{{ t "form.feed.label.blocklist_rules" }}
|
{{ t "form.feed.label.blocklist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -241,7 +243,7 @@
|
||||||
<label for="form-keeplist-rules">
|
<label for="form-keeplist-rules">
|
||||||
{{ t "form.feed.label.keeplist_rules" }}
|
{{ t "form.feed.label.keeplist_rules" }}
|
||||||
</label>
|
</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" }}
|
{{ icon "external-link" }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
{{ if .ShareCode }}
|
{{ if .ShareCode }}
|
||||||
<a href="{{ route "sharedEntry" "shareCode" .ShareCode }}"
|
<a href="{{ route "sharedEntry" "shareCode" .ShareCode }}"
|
||||||
title="{{ t "entry.shared_entry.title" }}"
|
title="{{ t "entry.shared_entry.title" }}"
|
||||||
target="_blank">{{ icon "share" }}</a>
|
{{ if $.user.OpenExternalLinksInNewTab }}target="_blank"{{ end }}>{{ icon "share" }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</h2>
|
</h2>
|
||||||
<span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
|
<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
|
CategoriesSortingOrder string
|
||||||
MarkReadOnView bool
|
MarkReadOnView bool
|
||||||
// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
|
// MarkReadBehavior is a string representation of the MarkReadOnView and MarkReadOnMediaPlayerCompletion fields together
|
||||||
MarkReadBehavior MarkReadBehavior
|
MarkReadBehavior MarkReadBehavior
|
||||||
MediaPlaybackRate float64
|
MediaPlaybackRate float64
|
||||||
BlockFilterEntryRules string
|
BlockFilterEntryRules string
|
||||||
KeepFilterEntryRules string
|
KeepFilterEntryRules string
|
||||||
AlwaysOpenExternalLinks bool
|
AlwaysOpenExternalLinks bool
|
||||||
|
OpenExternalLinksInNewTab bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
|
// 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.BlockFilterEntryRules = s.BlockFilterEntryRules
|
||||||
user.KeepFilterEntryRules = s.KeepFilterEntryRules
|
user.KeepFilterEntryRules = s.KeepFilterEntryRules
|
||||||
user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks
|
user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks
|
||||||
|
user.OpenExternalLinksInNewTab = s.OpenExternalLinksInNewTab
|
||||||
|
|
||||||
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
|
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior)
|
||||||
user.MarkReadOnView = MarkReadOnView
|
user.MarkReadOnView = MarkReadOnView
|
||||||
|
@ -181,32 +183,33 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
|
||||||
mediaPlaybackRate = 1
|
mediaPlaybackRate = 1
|
||||||
}
|
}
|
||||||
return &SettingsForm{
|
return &SettingsForm{
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
Confirmation: r.FormValue("confirmation"),
|
Confirmation: r.FormValue("confirmation"),
|
||||||
Theme: r.FormValue("theme"),
|
Theme: r.FormValue("theme"),
|
||||||
Language: r.FormValue("language"),
|
Language: r.FormValue("language"),
|
||||||
Timezone: r.FormValue("timezone"),
|
Timezone: r.FormValue("timezone"),
|
||||||
EntryDirection: r.FormValue("entry_direction"),
|
EntryDirection: r.FormValue("entry_direction"),
|
||||||
EntryOrder: r.FormValue("entry_order"),
|
EntryOrder: r.FormValue("entry_order"),
|
||||||
EntriesPerPage: int(entriesPerPage),
|
EntriesPerPage: int(entriesPerPage),
|
||||||
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
||||||
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
||||||
CustomCSS: r.FormValue("custom_css"),
|
CustomCSS: r.FormValue("custom_css"),
|
||||||
CustomJS: r.FormValue("custom_js"),
|
CustomJS: r.FormValue("custom_js"),
|
||||||
ExternalFontHosts: r.FormValue("external_font_hosts"),
|
ExternalFontHosts: r.FormValue("external_font_hosts"),
|
||||||
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
||||||
GestureNav: r.FormValue("gesture_nav"),
|
GestureNav: r.FormValue("gesture_nav"),
|
||||||
DisplayMode: r.FormValue("display_mode"),
|
DisplayMode: r.FormValue("display_mode"),
|
||||||
DefaultReadingSpeed: int(defaultReadingSpeed),
|
DefaultReadingSpeed: int(defaultReadingSpeed),
|
||||||
CJKReadingSpeed: int(cjkReadingSpeed),
|
CJKReadingSpeed: int(cjkReadingSpeed),
|
||||||
DefaultHomePage: r.FormValue("default_home_page"),
|
DefaultHomePage: r.FormValue("default_home_page"),
|
||||||
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
CategoriesSortingOrder: r.FormValue("categories_sorting_order"),
|
||||||
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
MarkReadOnView: r.FormValue("mark_read_on_view") == "1",
|
||||||
MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")),
|
MarkReadBehavior: MarkReadBehavior(r.FormValue("mark_read_behavior")),
|
||||||
MediaPlaybackRate: mediaPlaybackRate,
|
MediaPlaybackRate: mediaPlaybackRate,
|
||||||
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
||||||
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
||||||
AlwaysOpenExternalLinks: r.FormValue("always_open_external_links") == "1",
|
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{
|
settingsForm := form.SettingsForm{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Theme: user.Theme,
|
Theme: user.Theme,
|
||||||
Language: user.Language,
|
Language: user.Language,
|
||||||
Timezone: user.Timezone,
|
Timezone: user.Timezone,
|
||||||
EntryDirection: user.EntryDirection,
|
EntryDirection: user.EntryDirection,
|
||||||
EntryOrder: user.EntryOrder,
|
EntryOrder: user.EntryOrder,
|
||||||
EntriesPerPage: user.EntriesPerPage,
|
EntriesPerPage: user.EntriesPerPage,
|
||||||
KeyboardShortcuts: user.KeyboardShortcuts,
|
KeyboardShortcuts: user.KeyboardShortcuts,
|
||||||
ShowReadingTime: user.ShowReadingTime,
|
ShowReadingTime: user.ShowReadingTime,
|
||||||
CustomCSS: user.Stylesheet,
|
CustomCSS: user.Stylesheet,
|
||||||
CustomJS: user.CustomJS,
|
CustomJS: user.CustomJS,
|
||||||
ExternalFontHosts: user.ExternalFontHosts,
|
ExternalFontHosts: user.ExternalFontHosts,
|
||||||
EntrySwipe: user.EntrySwipe,
|
EntrySwipe: user.EntrySwipe,
|
||||||
GestureNav: user.GestureNav,
|
GestureNav: user.GestureNav,
|
||||||
DisplayMode: user.DisplayMode,
|
DisplayMode: user.DisplayMode,
|
||||||
DefaultReadingSpeed: user.DefaultReadingSpeed,
|
DefaultReadingSpeed: user.DefaultReadingSpeed,
|
||||||
CJKReadingSpeed: user.CJKReadingSpeed,
|
CJKReadingSpeed: user.CJKReadingSpeed,
|
||||||
DefaultHomePage: user.DefaultHomePage,
|
DefaultHomePage: user.DefaultHomePage,
|
||||||
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
CategoriesSortingOrder: user.CategoriesSortingOrder,
|
||||||
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
|
MarkReadBehavior: form.MarkAsReadBehavior(user.MarkReadOnView, user.MarkReadOnMediaPlayerCompletion),
|
||||||
MediaPlaybackRate: user.MediaPlaybackRate,
|
MediaPlaybackRate: user.MediaPlaybackRate,
|
||||||
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
||||||
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
||||||
AlwaysOpenExternalLinks: user.AlwaysOpenExternalLinks,
|
AlwaysOpenExternalLinks: user.AlwaysOpenExternalLinks,
|
||||||
|
OpenExternalLinksInNewTab: user.OpenExternalLinksInNewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
timezones, err := h.store.Timezones()
|
timezones, err := h.store.Timezones()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue