1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-11 17:51:01 +00:00

refactor: unexport symbols

This commit is contained in:
Julien Voisin 2025-08-08 02:27:04 +02:00 committed by GitHub
parent a4d51b5586
commit 566670cc06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 369 additions and 376 deletions

View file

@ -1,7 +0,0 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package config // import "miniflux.app/v2/internal/config"
// Opts holds parsed configuration options.
var Opts *options

View file

@ -100,6 +100,9 @@ type option struct {
Value any Value any
} }
// Opts holds parsed configuration options.
var Opts *options
// options contains configuration options. // options contains configuration options.
type options struct { type options struct {
HTTPS bool HTTPS bool

View file

@ -135,14 +135,14 @@ var (
) )
) )
// Collector represents a metric collector. // collector represents a metric collector.
type Collector struct { type collector struct {
store *storage.Storage store *storage.Storage
refreshInterval int refreshInterval int
} }
// NewCollector initializes a new metric collector. // NewCollector initializes a new metric collector.
func NewCollector(store *storage.Storage, refreshInterval int) *Collector { func NewCollector(store *storage.Storage, refreshInterval int) *collector {
prometheus.MustRegister(BackgroundFeedRefreshDuration) prometheus.MustRegister(BackgroundFeedRefreshDuration)
prometheus.MustRegister(ScraperRequestDuration) prometheus.MustRegister(ScraperRequestDuration)
prometheus.MustRegister(ArchiveEntriesDuration) prometheus.MustRegister(ArchiveEntriesDuration)
@ -158,11 +158,11 @@ func NewCollector(store *storage.Storage, refreshInterval int) *Collector {
prometheus.MustRegister(dbConnectionsMaxIdleTimeClosedGauge) prometheus.MustRegister(dbConnectionsMaxIdleTimeClosedGauge)
prometheus.MustRegister(dbConnectionsMaxLifetimeClosedGauge) prometheus.MustRegister(dbConnectionsMaxLifetimeClosedGauge)
return &Collector{store, refreshInterval} return &collector{store, refreshInterval}
} }
// GatherStorageMetrics polls the database to fetch metrics. // GatherStorageMetrics polls the database to fetch metrics.
func (c *Collector) GatherStorageMetrics() { func (c *collector) GatherStorageMetrics() {
for range time.Tick(time.Duration(c.refreshInterval) * time.Second) { for range time.Tick(time.Duration(c.refreshInterval) * time.Second) {
slog.Debug("Collecting metrics from the database") slog.Debug("Collecting metrics from the database")

View file

@ -10,7 +10,7 @@ import (
) )
// Specs: http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html // Specs: http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html
type Atom03Feed struct { type atom03Feed struct {
Version string `xml:"version,attr"` Version string `xml:"version,attr"`
// The "atom:id" element's content conveys a permanent, globally unique identifier for the feed. // The "atom:id" element's content conveys a permanent, globally unique identifier for the feed.
@ -21,14 +21,14 @@ type Atom03Feed struct {
// The "atom:title" element is a Content construct that conveys a human-readable title for the feed. // The "atom:title" element is a Content construct that conveys a human-readable title for the feed.
// atom:feed elements MUST contain exactly one atom:title element. // atom:feed elements MUST contain exactly one atom:title element.
// If the feed describes a Web resource, its content SHOULD be the same as that resource's title. // If the feed describes a Web resource, its content SHOULD be the same as that resource's title.
Title Atom03Content `xml:"http://purl.org/atom/ns# title"` Title atom03Content `xml:"http://purl.org/atom/ns# title"`
// The "atom:link" element is a Link construct that conveys a URI associated with the feed. // The "atom:link" element is a Link construct that conveys a URI associated with the feed.
// The nature of the relationship as well as the link itself is determined by the element's content. // The nature of the relationship as well as the link itself is determined by the element's content.
// atom:feed elements MUST contain at least one atom:link element with a rel attribute value of "alternate". // atom:feed elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
// atom:feed elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value. // atom:feed elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
// atom:feed elements MAY contain additional atom:link elements beyond those described above. // atom:feed elements MAY contain additional atom:link elements beyond those described above.
Links AtomLinks `xml:"http://purl.org/atom/ns# link"` Links atomLinks `xml:"http://purl.org/atom/ns# link"`
// The "atom:author" element is a Person construct that indicates the default author of the feed. // The "atom:author" element is a Person construct that indicates the default author of the feed.
// atom:feed elements MUST contain exactly one atom:author element, // atom:feed elements MUST contain exactly one atom:author element,
@ -38,10 +38,10 @@ type Atom03Feed struct {
// The "atom:entry" element's represents an individual entry that is contained by the feed. // The "atom:entry" element's represents an individual entry that is contained by the feed.
// atom:feed elements MAY contain one or more atom:entry elements. // atom:feed elements MAY contain one or more atom:entry elements.
Entries []Atom03Entry `xml:"http://purl.org/atom/ns# entry"` Entries []atom03Entry `xml:"http://purl.org/atom/ns# entry"`
} }
type Atom03Entry struct { type atom03Entry struct {
// The "atom:id" element's content conveys a permanent, globally unique identifier for the entry. // The "atom:id" element's content conveys a permanent, globally unique identifier for the entry.
// It MUST NOT change over time, even if other representations of the entry (such as a web representation pointed to by the entry's atom:link element) are relocated. // It MUST NOT change over time, even if other representations of the entry (such as a web representation pointed to by the entry's atom:link element) are relocated.
// If the same entry is syndicated in two atom:feeds published by the same entity, the entry's atom:id MUST be the same in both feeds. // If the same entry is syndicated in two atom:feeds published by the same entity, the entry's atom:id MUST be the same in both feeds.
@ -50,7 +50,7 @@ type Atom03Entry struct {
// The "atom:title" element is a Content construct that conveys a human-readable title for the entry. // The "atom:title" element is a Content construct that conveys a human-readable title for the entry.
// atom:entry elements MUST have exactly one "atom:title" element. // atom:entry elements MUST have exactly one "atom:title" element.
// If an entry describes a Web resource, its content SHOULD be the same as that resource's title. // If an entry describes a Web resource, its content SHOULD be the same as that resource's title.
Title Atom03Content `xml:"title"` Title atom03Content `xml:"title"`
// The "atom:modified" element is a Date construct that indicates the time that the entry was last modified. // The "atom:modified" element is a Date construct that indicates the time that the entry was last modified.
// atom:entry elements MUST contain an atom:modified element, but MUST NOT contain more than one. // atom:entry elements MUST contain an atom:modified element, but MUST NOT contain more than one.
@ -73,15 +73,15 @@ type Atom03Entry struct {
// atom:entry elements MUST contain at least one atom:link element with a rel attribute value of "alternate". // atom:entry elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
// atom:entry elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value. // atom:entry elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
// atom:entry elements MAY contain additional atom:link elements beyond those described above. // atom:entry elements MAY contain additional atom:link elements beyond those described above.
Links AtomLinks `xml:"link"` Links atomLinks `xml:"link"`
// The "atom:summary" element is a Content construct that conveys a short summary, abstract or excerpt of the entry. // The "atom:summary" element is a Content construct that conveys a short summary, abstract or excerpt of the entry.
// atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one. // atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one.
Summary Atom03Content `xml:"summary"` Summary atom03Content `xml:"summary"`
// The "atom:content" element is a Content construct that conveys the content of the entry. // The "atom:content" element is a Content construct that conveys the content of the entry.
// atom:entry elements MAY contain one or more atom:content elements. // atom:entry elements MAY contain one or more atom:content elements.
Content Atom03Content `xml:"content"` Content atom03Content `xml:"content"`
// The "atom:author" element is a Person construct that indicates the default author of the entry. // The "atom:author" element is a Person construct that indicates the default author of the entry.
// atom:entry elements MUST contain exactly one atom:author element, // atom:entry elements MUST contain exactly one atom:author element,
@ -90,7 +90,7 @@ type Atom03Entry struct {
Author AtomPerson `xml:"author"` Author AtomPerson `xml:"author"`
} }
type Atom03Content struct { type atom03Content struct {
// Content constructs MAY have a "type" attribute, whose value indicates the media type of the content. // Content constructs MAY have a "type" attribute, whose value indicates the media type of the content.
// When present, this attribute's value MUST be a registered media type [RFC2045]. // When present, this attribute's value MUST be a registered media type [RFC2045].
// If not present, its value MUST be considered to be "text/plain". // If not present, its value MUST be considered to be "text/plain".
@ -113,7 +113,7 @@ type Atom03Content struct {
InnerXML string `xml:",innerxml"` InnerXML string `xml:",innerxml"`
} }
func (a *Atom03Content) Content() string { func (a *atom03Content) content() string {
content := "" content := ""
switch a.Mode { switch a.Mode {

View file

@ -14,15 +14,16 @@ import (
"miniflux.app/v2/internal/urllib" "miniflux.app/v2/internal/urllib"
) )
type Atom03Adapter struct { type atom03Adapter struct {
atomFeed *Atom03Feed atomFeed *atom03Feed
} }
func NewAtom03Adapter(atomFeed *Atom03Feed) *Atom03Adapter { // TODO No need for a constructor, as it's only used in this package
return &Atom03Adapter{atomFeed} func NewAtom03Adapter(atomFeed *atom03Feed) *atom03Adapter {
return &atom03Adapter{atomFeed}
} }
func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed { func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
feed := new(model.Feed) feed := new(model.Feed)
// Populate the feed URL. // Populate the feed URL.
@ -36,7 +37,7 @@ func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Populate the site URL. // Populate the site URL.
siteURL := a.atomFeed.Links.OriginalLink() siteURL := a.atomFeed.Links.originalLink()
if siteURL != "" { if siteURL != "" {
if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil { if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
feed.SiteURL = absoluteSiteURL feed.SiteURL = absoluteSiteURL
@ -46,7 +47,7 @@ func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Populate the feed title. // Populate the feed title.
feed.Title = a.atomFeed.Title.Content() feed.Title = a.atomFeed.Title.content()
if feed.Title == "" { if feed.Title == "" {
feed.Title = feed.SiteURL feed.Title = feed.SiteURL
} }
@ -55,7 +56,7 @@ func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
entry := model.NewEntry() entry := model.NewEntry()
// Populate the entry URL. // Populate the entry URL.
entry.URL = atomEntry.Links.OriginalLink() entry.URL = atomEntry.Links.originalLink()
if entry.URL != "" { if entry.URL != "" {
if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil { if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil {
entry.URL = absoluteEntryURL entry.URL = absoluteEntryURL
@ -63,13 +64,13 @@ func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Populate the entry content. // Populate the entry content.
entry.Content = atomEntry.Content.Content() entry.Content = atomEntry.Content.content()
if entry.Content == "" { if entry.Content == "" {
entry.Content = atomEntry.Summary.Content() entry.Content = atomEntry.Summary.content()
} }
// Populate the entry title. // Populate the entry title.
entry.Title = atomEntry.Title.Content() entry.Title = atomEntry.Title.content()
if entry.Title == "" { if entry.Title == "" {
entry.Title = sanitizer.TruncateHTML(entry.Content, 100) entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
} }
@ -101,7 +102,7 @@ func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Generate the entry hash. // Generate the entry hash.
for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} { for _, value := range []string{atomEntry.ID, atomEntry.Links.originalLink()} {
if value != "" { if value != "" {
entry.Hash = crypto.SHA256(value) entry.Hash = crypto.SHA256(value)
break break

View file

@ -19,7 +19,7 @@ import (
// Specs: // Specs:
// https://tools.ietf.org/html/rfc4287 // https://tools.ietf.org/html/rfc4287
// https://validator.w3.org/feed/docs/atom.html // https://validator.w3.org/feed/docs/atom.html
type Atom10Feed struct { type atom10Feed struct {
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"` XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
// The "atom:id" element conveys a permanent, universally unique // The "atom:id" element conveys a permanent, universally unique
@ -37,11 +37,11 @@ type Atom10Feed struct {
// readable title for an entry or feed. // readable title for an entry or feed.
// //
// atom:feed elements MUST contain exactly one atom:title element. // atom:feed elements MUST contain exactly one atom:title element.
Title Atom10Text `xml:"http://www.w3.org/2005/Atom title"` Title atom10Text `xml:"http://www.w3.org/2005/Atom title"`
// The "atom:subtitle" element is a Text construct that // The "atom:subtitle" element is a Text construct that
// contains a human-readable description or subtitle for the feed. // contains a human-readable description or subtitle for the feed.
Subtitle Atom10Text `xml:"http://www.w3.org/2005/Atom subtitle"` Subtitle atom10Text `xml:"http://www.w3.org/2005/Atom subtitle"`
// The "atom:author" element is a Person construct that indicates the // The "atom:author" element is a Person construct that indicates the
// author of the entry or feed. // author of the entry or feed.
@ -49,7 +49,7 @@ type Atom10Feed struct {
// atom:feed elements MUST contain one or more atom:author elements, // atom:feed elements MUST contain one or more atom:author elements,
// unless all of the atom:feed element's child atom:entry elements // unless all of the atom:feed element's child atom:entry elements
// contain at least one atom:author element. // contain at least one atom:author element.
Authors AtomPersons `xml:"http://www.w3.org/2005/Atom author"` Authors atomPersons `xml:"http://www.w3.org/2005/Atom author"`
// The "atom:icon" element's content is an IRI reference [RFC3987] that // The "atom:icon" element's content is an IRI reference [RFC3987] that
// identifies an image that provides iconic visual identification for a // identifies an image that provides iconic visual identification for a
@ -71,7 +71,7 @@ type Atom10Feed struct {
// atom:feed elements MUST NOT contain more than one atom:link // atom:feed elements MUST NOT contain more than one atom:link
// element with a rel attribute value of "alternate" that has the // element with a rel attribute value of "alternate" that has the
// same combination of type and hreflang attribute values. // same combination of type and hreflang attribute values.
Links AtomLinks `xml:"http://www.w3.org/2005/Atom link"` Links atomLinks `xml:"http://www.w3.org/2005/Atom link"`
// The "atom:category" element conveys information about a category // The "atom:category" element conveys information about a category
// associated with an entry or feed. This specification assigns no // associated with an entry or feed. This specification assigns no
@ -79,12 +79,12 @@ type Atom10Feed struct {
// //
// atom:feed elements MAY contain any number of atom:category // atom:feed elements MAY contain any number of atom:category
// elements. // elements.
Categories AtomCategories `xml:"http://www.w3.org/2005/Atom category"` Categories atomCategories `xml:"http://www.w3.org/2005/Atom category"`
Entries []Atom10Entry `xml:"http://www.w3.org/2005/Atom entry"` Entries []atom10Entry `xml:"http://www.w3.org/2005/Atom entry"`
} }
type Atom10Entry struct { type atom10Entry struct {
// The "atom:id" element conveys a permanent, universally unique // The "atom:id" element conveys a permanent, universally unique
// identifier for an entry or feed. // identifier for an entry or feed.
// //
@ -100,7 +100,7 @@ type Atom10Entry struct {
// readable title for an entry or feed. // readable title for an entry or feed.
// //
// atom:entry elements MUST contain exactly one atom:title element. // atom:entry elements MUST contain exactly one atom:title element.
Title Atom10Text `xml:"http://www.w3.org/2005/Atom title"` Title atom10Text `xml:"http://www.w3.org/2005/Atom title"`
// The "atom:published" element is a Date construct indicating an // The "atom:published" element is a Date construct indicating an
// instant in time associated with an event early in the life cycle of // instant in time associated with an event early in the life cycle of
@ -118,7 +118,7 @@ type Atom10Entry struct {
// atom:entry elements MUST NOT contain more than one atom:link // atom:entry elements MUST NOT contain more than one atom:link
// element with a rel attribute value of "alternate" that has the // element with a rel attribute value of "alternate" that has the
// same combination of type and hreflang attribute values. // same combination of type and hreflang attribute values.
Links AtomLinks `xml:"http://www.w3.org/2005/Atom link"` Links atomLinks `xml:"http://www.w3.org/2005/Atom link"`
// atom:entry elements MUST contain an atom:summary element in either // atom:entry elements MUST contain an atom:summary element in either
// of the following cases: // of the following cases:
@ -131,17 +131,17 @@ type Atom10Entry struct {
// //
// atom:entry elements MUST NOT contain more than one atom:summary // atom:entry elements MUST NOT contain more than one atom:summary
// element. // element.
Summary Atom10Text `xml:"http://www.w3.org/2005/Atom summary"` Summary atom10Text `xml:"http://www.w3.org/2005/Atom summary"`
// atom:entry elements MUST NOT contain more than one atom:content // atom:entry elements MUST NOT contain more than one atom:content
// element. // element.
Content Atom10Text `xml:"http://www.w3.org/2005/Atom content"` Content atom10Text `xml:"http://www.w3.org/2005/Atom content"`
// The "atom:author" element is a Person construct that indicates the // The "atom:author" element is a Person construct that indicates the
// author of the entry or feed. // author of the entry or feed.
// //
// atom:entry elements MUST contain one or more atom:author elements // atom:entry elements MUST contain one or more atom:author elements
Authors AtomPersons `xml:"http://www.w3.org/2005/Atom author"` Authors atomPersons `xml:"http://www.w3.org/2005/Atom author"`
// The "atom:category" element conveys information about a category // The "atom:category" element conveys information about a category
// associated with an entry or feed. This specification assigns no // associated with an entry or feed. This specification assigns no
@ -149,7 +149,7 @@ type Atom10Entry struct {
// //
// atom:entry elements MAY contain any number of atom:category // atom:entry elements MAY contain any number of atom:category
// elements. // elements.
Categories AtomCategories `xml:"http://www.w3.org/2005/Atom category"` Categories atomCategories `xml:"http://www.w3.org/2005/Atom category"`
media.MediaItemElement media.MediaItemElement
} }
@ -160,14 +160,14 @@ type Atom10Entry struct {
// Text: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.1 // Text: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.1
// HTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.2 // HTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.2
// XHTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.3 // XHTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.3
type Atom10Text struct { type atom10Text struct {
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
CharData string `xml:",chardata"` CharData string `xml:",chardata"`
InnerXML string `xml:",innerxml"` InnerXML string `xml:",innerxml"`
XHTMLRootElement AtomXHTMLRootElement `xml:"http://www.w3.org/1999/xhtml div"` XHTMLRootElement atomXHTMLRootElement `xml:"http://www.w3.org/1999/xhtml div"`
} }
func (a *Atom10Text) Body() string { func (a *atom10Text) body() string {
var content string var content string
if strings.EqualFold(a.Type, "xhtml") { if strings.EqualFold(a.Type, "xhtml") {
@ -179,7 +179,7 @@ func (a *Atom10Text) Body() string {
return strings.TrimSpace(content) return strings.TrimSpace(content)
} }
func (a *Atom10Text) Title() string { func (a *atom10Text) title() string {
var content string var content string
switch { switch {
@ -194,14 +194,14 @@ func (a *Atom10Text) Title() string {
return strings.TrimSpace(content) return strings.TrimSpace(content)
} }
func (a *Atom10Text) xhtmlContent() string { func (a *atom10Text) xhtmlContent() string {
if a.XHTMLRootElement.XMLName.Local == "div" { if a.XHTMLRootElement.XMLName.Local == "div" {
return a.XHTMLRootElement.InnerXML return a.XHTMLRootElement.InnerXML
} }
return a.InnerXML return a.InnerXML
} }
type AtomXHTMLRootElement struct { type atomXHTMLRootElement struct {
XMLName xml.Name `xml:"div"` XMLName xml.Name `xml:"div"`
InnerXML string `xml:",innerxml"` InnerXML string `xml:",innerxml"`
} }

View file

@ -19,10 +19,10 @@ import (
) )
type Atom10Adapter struct { type Atom10Adapter struct {
atomFeed *Atom10Feed atomFeed *atom10Feed
} }
func NewAtom10Adapter(atomFeed *Atom10Feed) *Atom10Adapter { func NewAtom10Adapter(atomFeed *atom10Feed) *Atom10Adapter {
return &Atom10Adapter{atomFeed} return &Atom10Adapter{atomFeed}
} }
@ -40,7 +40,7 @@ func (a *Atom10Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Populate the site URL. // Populate the site URL.
siteURL := a.atomFeed.Links.OriginalLink() siteURL := a.atomFeed.Links.originalLink()
if siteURL != "" { if siteURL != "" {
if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil { if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
feed.SiteURL = absoluteSiteURL feed.SiteURL = absoluteSiteURL
@ -50,13 +50,13 @@ func (a *Atom10Adapter) BuildFeed(baseURL string) *model.Feed {
} }
// Populate the feed title. // Populate the feed title.
feed.Title = a.atomFeed.Title.Body() feed.Title = a.atomFeed.Title.body()
if feed.Title == "" { if feed.Title == "" {
feed.Title = feed.SiteURL feed.Title = feed.SiteURL
} }
// Populate the feed description. // Populate the feed description.
feed.Description = a.atomFeed.Subtitle.Body() feed.Description = a.atomFeed.Subtitle.body()
// Populate the feed icon. // Populate the feed icon.
if a.atomFeed.Icon != "" { if a.atomFeed.Icon != "" {
@ -79,7 +79,7 @@ func (a *Atom10Adapter) populateEntries(siteURL string) model.Entries {
entry := model.NewEntry() entry := model.NewEntry()
// Populate the entry URL. // Populate the entry URL.
entry.URL = atomEntry.Links.OriginalLink() entry.URL = atomEntry.Links.originalLink()
if entry.URL != "" { if entry.URL != "" {
if absoluteEntryURL, err := urllib.AbsoluteURL(siteURL, entry.URL); err == nil { if absoluteEntryURL, err := urllib.AbsoluteURL(siteURL, entry.URL); err == nil {
entry.URL = absoluteEntryURL entry.URL = absoluteEntryURL
@ -87,16 +87,16 @@ func (a *Atom10Adapter) populateEntries(siteURL string) model.Entries {
} }
// Populate the entry content. // Populate the entry content.
entry.Content = atomEntry.Content.Body() entry.Content = atomEntry.Content.body()
if entry.Content == "" { if entry.Content == "" {
entry.Content = atomEntry.Summary.Body() entry.Content = atomEntry.Summary.body()
if entry.Content == "" { if entry.Content == "" {
entry.Content = atomEntry.FirstMediaDescription() entry.Content = atomEntry.FirstMediaDescription()
} }
} }
// Populate the entry title. // Populate the entry title.
entry.Title = atomEntry.Title.Title() entry.Title = atomEntry.Title.title()
if entry.Title == "" { if entry.Title == "" {
entry.Title = sanitizer.TruncateHTML(entry.Content, 100) entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
if entry.Title == "" { if entry.Title == "" {
@ -105,9 +105,9 @@ func (a *Atom10Adapter) populateEntries(siteURL string) model.Entries {
} }
// Populate the entry author. // Populate the entry author.
authors := atomEntry.Authors.PersonNames() authors := atomEntry.Authors.personNames()
if len(authors) == 0 { if len(authors) == 0 {
authors = a.atomFeed.Authors.PersonNames() authors = a.atomFeed.Authors.personNames()
} }
sort.Strings(authors) sort.Strings(authors)
authors = slices.Compact(authors) authors = slices.Compact(authors)
@ -152,7 +152,7 @@ func (a *Atom10Adapter) populateEntries(siteURL string) model.Entries {
} }
// Generate the entry hash. // Generate the entry hash.
for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} { for _, value := range []string{atomEntry.ID, atomEntry.Links.originalLink()} {
if value != "" { if value != "" {
entry.Hash = crypto.SHA256(value) entry.Hash = crypto.SHA256(value)
break break

View file

@ -30,9 +30,9 @@ func (a *AtomPerson) PersonName() string {
return strings.TrimSpace(a.Email) return strings.TrimSpace(a.Email)
} }
type AtomPersons []*AtomPerson type atomPersons []*AtomPerson
func (a AtomPersons) PersonNames() []string { func (a atomPersons) personNames() []string {
var names []string var names []string
authorNamesMap := make(map[string]bool) authorNamesMap := make(map[string]bool)
@ -56,9 +56,9 @@ type AtomLink struct {
Title string `xml:"title,attr"` Title string `xml:"title,attr"`
} }
type AtomLinks []*AtomLink type atomLinks []*AtomLink
func (a AtomLinks) OriginalLink() string { func (a atomLinks) originalLink() string {
for _, link := range a { for _, link := range a {
if strings.EqualFold(link.Rel, "alternate") { if strings.EqualFold(link.Rel, "alternate") {
return strings.TrimSpace(link.Href) return strings.TrimSpace(link.Href)
@ -72,7 +72,7 @@ func (a AtomLinks) OriginalLink() string {
return "" return ""
} }
func (a AtomLinks) firstLinkWithRelation(relation string) string { func (a atomLinks) firstLinkWithRelation(relation string) string {
for _, link := range a { for _, link := range a {
if strings.EqualFold(link.Rel, relation) { if strings.EqualFold(link.Rel, relation) {
return strings.TrimSpace(link.Href) return strings.TrimSpace(link.Href)
@ -82,7 +82,7 @@ func (a AtomLinks) firstLinkWithRelation(relation string) string {
return "" return ""
} }
func (a AtomLinks) firstLinkWithRelationAndType(relation string, contentTypes ...string) string { func (a atomLinks) firstLinkWithRelationAndType(relation string, contentTypes ...string) string {
for _, link := range a { for _, link := range a {
if strings.EqualFold(link.Rel, relation) { if strings.EqualFold(link.Rel, relation) {
for _, contentType := range contentTypes { for _, contentType := range contentTypes {
@ -96,7 +96,7 @@ func (a AtomLinks) firstLinkWithRelationAndType(relation string, contentTypes ..
return "" return ""
} }
func (a AtomLinks) findAllLinksWithRelation(relation string) []*AtomLink { func (a atomLinks) findAllLinksWithRelation(relation string) []*AtomLink {
var links []*AtomLink var links []*AtomLink
for _, link := range a { for _, link := range a {
@ -116,7 +116,7 @@ func (a AtomLinks) findAllLinksWithRelation(relation string) []*AtomLink {
// meaning to the content (if any) of this element. // meaning to the content (if any) of this element.
// //
// Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.2 // Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.2
type AtomCategory struct { type atomCategory struct {
// The "term" attribute is a string that identifies the category to // The "term" attribute is a string that identifies the category to
// which the entry or feed belongs. Category elements MUST have a // which the entry or feed belongs. Category elements MUST have a
// "term" attribute. // "term" attribute.
@ -134,9 +134,9 @@ type AtomCategory struct {
Label string `xml:"label,attr"` Label string `xml:"label,attr"`
} }
type AtomCategories []AtomCategory type atomCategories []atomCategory
func (ac AtomCategories) CategoryNames() []string { func (ac atomCategories) CategoryNames() []string {
var categories []string var categories []string
for _, category := range ac { for _, category := range ac {

View file

@ -15,13 +15,13 @@ import (
func Parse(baseURL string, r io.ReadSeeker, version string) (*model.Feed, error) { func Parse(baseURL string, r io.ReadSeeker, version string) (*model.Feed, error) {
switch version { switch version {
case "0.3": case "0.3":
atomFeed := new(Atom03Feed) atomFeed := new(atom03Feed)
if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil { if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
return nil, fmt.Errorf("atom: unable to parse Atom 0.3 feed: %w", err) return nil, fmt.Errorf("atom: unable to parse Atom 0.3 feed: %w", err)
} }
return NewAtom03Adapter(atomFeed).BuildFeed(baseURL), nil return NewAtom03Adapter(atomFeed).buildFeed(baseURL), nil
default: default:
atomFeed := new(Atom10Feed) atomFeed := new(atom10Feed)
if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil { if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
return nil, fmt.Errorf("atom: unable to parse Atom 1.0 feed: %w", err) return nil, fmt.Errorf("atom: unable to parse Atom 1.0 feed: %w", err)
} }

View file

@ -23,9 +23,9 @@ func (h *Handler) Export(userID int64) (string, error) {
return "", err return "", err
} }
subscriptions := make(SubcriptionList, 0, len(feeds)) subscriptions := make(subcriptionList, 0, len(feeds))
for _, feed := range feeds { for _, feed := range feeds {
subscriptions = append(subscriptions, &Subcription{ subscriptions = append(subscriptions, &subcription{
Title: feed.Title, Title: feed.Title,
FeedURL: feed.FeedURL, FeedURL: feed.FeedURL,
SiteURL: feed.SiteURL, SiteURL: feed.SiteURL,
@ -34,12 +34,12 @@ func (h *Handler) Export(userID int64) (string, error) {
}) })
} }
return Serialize(subscriptions), nil return serialize(subscriptions), nil
} }
// Import parses and create feeds from an OPML import. // Import parses and create feeds from an OPML import.
func (h *Handler) Import(userID int64, data io.Reader) error { func (h *Handler) Import(userID int64, data io.Reader) error {
subscriptions, err := Parse(data) subscriptions, err := parse(data)
if err != nil { if err != nil {
return err return err
} }

View file

@ -16,6 +16,7 @@ type opmlDocument struct {
Outlines opmlOutlineCollection `xml:"body>outline"` Outlines opmlOutlineCollection `xml:"body>outline"`
} }
// TODO remove as this is only used in the opml package
func NewOPMLDocument() *opmlDocument { func NewOPMLDocument() *opmlDocument {
return &opmlDocument{} return &opmlDocument{}
} }

View file

@ -11,8 +11,8 @@ import (
"miniflux.app/v2/internal/reader/encoding" "miniflux.app/v2/internal/reader/encoding"
) )
// Parse reads an OPML file and returns a SubcriptionList. // parse reads an OPML file and returns a SubcriptionList.
func Parse(data io.Reader) (SubcriptionList, error) { func parse(data io.Reader) (subcriptionList, error) {
opmlDocument := NewOPMLDocument() opmlDocument := NewOPMLDocument()
decoder := xml.NewDecoder(data) decoder := xml.NewDecoder(data)
decoder.Entity = xml.HTMLEntity decoder.Entity = xml.HTMLEntity
@ -27,10 +27,10 @@ func Parse(data io.Reader) (SubcriptionList, error) {
return getSubscriptionsFromOutlines(opmlDocument.Outlines, ""), nil return getSubscriptionsFromOutlines(opmlDocument.Outlines, ""), nil
} }
func getSubscriptionsFromOutlines(outlines opmlOutlineCollection, category string) (subscriptions SubcriptionList) { func getSubscriptionsFromOutlines(outlines opmlOutlineCollection, category string) (subscriptions subcriptionList) {
for _, outline := range outlines { for _, outline := range outlines {
if outline.IsSubscription() { if outline.IsSubscription() {
subscriptions = append(subscriptions, &Subcription{ subscriptions = append(subscriptions, &subcription{
Title: outline.GetTitle(), Title: outline.GetTitle(),
FeedURL: outline.FeedURL, FeedURL: outline.FeedURL,
SiteURL: outline.GetSiteURL(), SiteURL: outline.GetSiteURL(),

View file

@ -8,6 +8,13 @@ import (
"testing" "testing"
) )
// equals compare two subscriptions.
func (s subcription) equals(subscription *subcription) bool {
return s.Title == subscription.Title && s.SiteURL == subscription.SiteURL &&
s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName &&
s.Description == subscription.Description
}
func TestParseOpmlWithoutCategories(t *testing.T) { func TestParseOpmlWithoutCategories(t *testing.T) {
data := `<?xml version="1.0" encoding="ISO-8859-1"?> data := `<?xml version="1.0" encoding="ISO-8859-1"?>
<opml version="2.0"> <opml version="2.0">
@ -32,10 +39,10 @@ func TestParseOpmlWithoutCategories(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "CNET News.com", FeedURL: "http://news.com.com/2547-1_3-0-5.xml", SiteURL: "http://news.com.com/", Description: "Tech news and business reports by CNET News.com. Focused on information technology, core topics include computers, hardware, software, networking, and Internet media."}) expected = append(expected, &subcription{Title: "CNET News.com", FeedURL: "http://news.com.com/2547-1_3-0-5.xml", SiteURL: "http://news.com.com/", Description: "Tech news and business reports by CNET News.com. Focused on information technology, core topics include computers, hardware, software, networking, and Internet media."})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -44,7 +51,7 @@ func TestParseOpmlWithoutCategories(t *testing.T) {
t.Fatalf("Wrong number of subscriptions: %d instead of %d", len(subscriptions), 13) t.Fatalf("Wrong number of subscriptions: %d instead of %d", len(subscriptions), 13)
} }
if !subscriptions[0].Equals(expected[0]) { if !subscriptions[0].equals(expected[0]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[0], expected[0]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[0], expected[0])
} }
} }
@ -67,12 +74,12 @@ func TestParseOpmlWithCategories(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "My Category 1"}) expected = append(expected, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "My Category 1"})
expected = append(expected, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "My Category 1"}) expected = append(expected, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "My Category 1"})
expected = append(expected, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed3/", SiteURL: "http://example.org/3", CategoryName: "My Category 2"}) expected = append(expected, &subcription{Title: "Feed 3", FeedURL: "http://example.org/feed3/", SiteURL: "http://example.org/3", CategoryName: "My Category 2"})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -82,7 +89,7 @@ func TestParseOpmlWithCategories(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -101,11 +108,11 @@ func TestParseOpmlWithEmptyTitleAndEmptySiteURL(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "http://example.org/1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: ""}) expected = append(expected, &subcription{Title: "http://example.org/1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: ""})
expected = append(expected, &Subcription{Title: "http://example.org/feed2/", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/feed2/", CategoryName: ""}) expected = append(expected, &subcription{Title: "http://example.org/feed2/", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/feed2/", CategoryName: ""})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -115,7 +122,7 @@ func TestParseOpmlWithEmptyTitleAndEmptySiteURL(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -139,11 +146,11 @@ func TestParseOpmlVersion1(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "Category 1"}) expected = append(expected, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "Category 1"})
expected = append(expected, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "Category 2"}) expected = append(expected, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "Category 2"})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -153,7 +160,7 @@ func TestParseOpmlVersion1(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -173,11 +180,11 @@ func TestParseOpmlVersion1WithoutOuterOutline(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: ""}) expected = append(expected, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: ""})
expected = append(expected, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: ""}) expected = append(expected, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: ""})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -187,7 +194,7 @@ func TestParseOpmlVersion1WithoutOuterOutline(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -214,12 +221,12 @@ func TestParseOpmlVersion1WithSeveralNestedOutlines(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "Some Category"}) expected = append(expected, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/", SiteURL: "http://example.org/1", CategoryName: "Some Category"})
expected = append(expected, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "Some Category"}) expected = append(expected, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed2/", SiteURL: "http://example.org/2", CategoryName: "Some Category"})
expected = append(expected, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed3/", SiteURL: "http://example.org/3", CategoryName: "Another Category"}) expected = append(expected, &subcription{Title: "Feed 3", FeedURL: "http://example.org/feed3/", SiteURL: "http://example.org/3", CategoryName: "Another Category"})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -229,7 +236,7 @@ func TestParseOpmlVersion1WithSeveralNestedOutlines(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -249,10 +256,10 @@ func TestParseOpmlWithInvalidCharacterEntity(t *testing.T) {
</opml> </opml>
` `
var expected SubcriptionList var expected subcriptionList
expected = append(expected, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/a&b", SiteURL: "http://example.org/c&d", CategoryName: "Feed 1"}) expected = append(expected, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed1/a&b", SiteURL: "http://example.org/c&d", CategoryName: "Feed 1"})
subscriptions, err := Parse(bytes.NewBufferString(data)) subscriptions, err := parse(bytes.NewBufferString(data))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -262,7 +269,7 @@ func TestParseOpmlWithInvalidCharacterEntity(t *testing.T) {
} }
for i := range len(subscriptions) { for i := range len(subscriptions) {
if !subscriptions[i].Equals(expected[i]) { if !subscriptions[i].equals(expected[i]) {
t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i]) t.Errorf(`Subscription is different: "%v" vs "%v"`, subscriptions[i], expected[i])
} }
} }
@ -270,7 +277,7 @@ func TestParseOpmlWithInvalidCharacterEntity(t *testing.T) {
func TestParseInvalidXML(t *testing.T) { func TestParseInvalidXML(t *testing.T) {
data := `garbage` data := `garbage`
_, err := Parse(bytes.NewBufferString(data)) _, err := parse(bytes.NewBufferString(data))
if err == nil { if err == nil {
t.Error("Parse should generate an error") t.Error("Parse should generate an error")
} }

View file

@ -12,8 +12,8 @@ import (
"time" "time"
) )
// Serialize returns a SubcriptionList in OPML format. // serialize returns a SubcriptionList in OPML format.
func Serialize(subscriptions SubcriptionList) string { func serialize(subscriptions subcriptionList) string {
var b bytes.Buffer var b bytes.Buffer
writer := bufio.NewWriter(&b) writer := bufio.NewWriter(&b)
writer.WriteString(xml.Header) writer.WriteString(xml.Header)
@ -31,7 +31,7 @@ func Serialize(subscriptions SubcriptionList) string {
return b.String() return b.String()
} }
func convertSubscriptionsToOPML(subscriptions SubcriptionList) *opmlDocument { func convertSubscriptionsToOPML(subscriptions subcriptionList) *opmlDocument {
opmlDocument := NewOPMLDocument() opmlDocument := NewOPMLDocument()
opmlDocument.Version = "2.0" opmlDocument.Version = "2.0"
opmlDocument.Header.Title = "Miniflux" opmlDocument.Header.Title = "Miniflux"
@ -62,8 +62,8 @@ func convertSubscriptionsToOPML(subscriptions SubcriptionList) *opmlDocument {
return opmlDocument return opmlDocument
} }
func groupSubscriptionsByFeed(subscriptions SubcriptionList) map[string]SubcriptionList { func groupSubscriptionsByFeed(subscriptions subcriptionList) map[string]subcriptionList {
groups := make(map[string]SubcriptionList) groups := make(map[string]subcriptionList)
for _, subscription := range subscriptions { for _, subscription := range subscriptions {
groups[subscription.CategoryName] = append(groups[subscription.CategoryName], subscription) groups[subscription.CategoryName] = append(groups[subscription.CategoryName], subscription)

View file

@ -9,13 +9,13 @@ import (
) )
func TestSerialize(t *testing.T) { func TestSerialize(t *testing.T) {
var subscriptions SubcriptionList var subscriptions subcriptionList
subscriptions = append(subscriptions, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed/1", SiteURL: "http://example.org/1", CategoryName: "Category 1"}) subscriptions = append(subscriptions, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed/1", SiteURL: "http://example.org/1", CategoryName: "Category 1"})
subscriptions = append(subscriptions, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed/2", SiteURL: "http://example.org/2", CategoryName: "Category 1"}) subscriptions = append(subscriptions, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed/2", SiteURL: "http://example.org/2", CategoryName: "Category 1"})
subscriptions = append(subscriptions, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: "Category 2"}) subscriptions = append(subscriptions, &subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: "Category 2"})
output := Serialize(subscriptions) output := serialize(subscriptions)
feeds, err := Parse(bytes.NewBufferString(output)) feeds, err := parse(bytes.NewBufferString(output))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -48,10 +48,10 @@ func TestNormalizedCategoriesOrder(t *testing.T) {
{"Category 1", "Category 3"}, {"Category 1", "Category 3"},
} }
var subscriptions SubcriptionList var subscriptions subcriptionList
subscriptions = append(subscriptions, &Subcription{Title: "Feed 1", FeedURL: "http://example.org/feed/1", SiteURL: "http://example.org/1", CategoryName: orderTests[0].naturalOrderName}) subscriptions = append(subscriptions, &subcription{Title: "Feed 1", FeedURL: "http://example.org/feed/1", SiteURL: "http://example.org/1", CategoryName: orderTests[0].naturalOrderName})
subscriptions = append(subscriptions, &Subcription{Title: "Feed 2", FeedURL: "http://example.org/feed/2", SiteURL: "http://example.org/2", CategoryName: orderTests[1].naturalOrderName}) subscriptions = append(subscriptions, &subcription{Title: "Feed 2", FeedURL: "http://example.org/feed/2", SiteURL: "http://example.org/2", CategoryName: orderTests[1].naturalOrderName})
subscriptions = append(subscriptions, &Subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: orderTests[2].naturalOrderName}) subscriptions = append(subscriptions, &subcription{Title: "Feed 3", FeedURL: "http://example.org/feed/3", SiteURL: "http://example.org/3", CategoryName: orderTests[2].naturalOrderName})
feeds := convertSubscriptionsToOPML(subscriptions) feeds := convertSubscriptionsToOPML(subscriptions)

View file

@ -3,8 +3,8 @@
package opml // import "miniflux.app/v2/internal/reader/opml" package opml // import "miniflux.app/v2/internal/reader/opml"
// Subcription represents a feed that will be imported or exported. // subcription represents a feed that will be imported or exported.
type Subcription struct { type subcription struct {
Title string Title string
SiteURL string SiteURL string
FeedURL string FeedURL string
@ -12,12 +12,5 @@ type Subcription struct {
Description string Description string
} }
// Equals compare two subscriptions. // subcriptionList is a list of subscriptions.
func (s Subcription) Equals(subscription *Subcription) bool { type subcriptionList []*subcription
return s.Title == subscription.Title && s.SiteURL == subscription.SiteURL &&
s.FeedURL == subscription.FeedURL && s.CategoryName == subscription.CategoryName &&
s.Description == subscription.Description
}
// SubcriptionList is a list of subscriptions.
type SubcriptionList []*Subcription

View file

@ -16,15 +16,11 @@ import (
"miniflux.app/v2/internal/urllib" "miniflux.app/v2/internal/urllib"
) )
type RDFAdapter struct { type rdfAdapter struct {
rdf *RDF rdf *rdf
} }
func NewRDFAdapter(rdf *RDF) *RDFAdapter { func (r *rdfAdapter) buildFeed(baseURL string) *model.Feed {
return &RDFAdapter{rdf}
}
func (r *RDFAdapter) BuildFeed(baseURL string) *model.Feed {
feed := &model.Feed{ feed := &model.Feed{
Title: stripTags(r.rdf.Channel.Title), Title: stripTags(r.rdf.Channel.Title),
FeedURL: strings.TrimSpace(baseURL), FeedURL: strings.TrimSpace(baseURL),

View file

@ -13,10 +13,11 @@ import (
// Parse returns a normalized feed struct from a RDF feed. // Parse returns a normalized feed struct from a RDF feed.
func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) { func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) {
xmlFeed := new(RDF) xmlFeed := new(rdf)
if err := xml.NewXMLDecoder(data).Decode(xmlFeed); err != nil { if err := xml.NewXMLDecoder(data).Decode(xmlFeed); err != nil {
return nil, fmt.Errorf("rdf: unable to parse feed: %w", err) return nil, fmt.Errorf("rdf: unable to parse feed: %w", err)
} }
return NewRDFAdapter(xmlFeed).BuildFeed(baseURL), nil adapter := &rdfAdapter{xmlFeed}
return adapter.buildFeed(baseURL), nil
} }

View file

@ -9,21 +9,21 @@ import (
"miniflux.app/v2/internal/reader/dublincore" "miniflux.app/v2/internal/reader/dublincore"
) )
// RDF sepcs: https://web.resource.org/rss/1.0/spec // rdf sepcs: https://web.resource.org/rss/1.0/spec
type RDF struct { type rdf struct {
XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF"` XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF"`
Channel RDFChannel `xml:"channel"` Channel rdfChannel `xml:"channel"`
Items []RDFItem `xml:"item"` Items []rdfItem `xml:"item"`
} }
type RDFChannel struct { type rdfChannel struct {
Title string `xml:"title"` Title string `xml:"title"`
Link string `xml:"link"` Link string `xml:"link"`
Description string `xml:"description"` Description string `xml:"description"`
dublincore.DublinCoreChannelElement dublincore.DublinCoreChannelElement
} }
type RDFItem struct { type rdfItem struct {
Title string `xml:"http://purl.org/rss/1.0/ title"` Title string `xml:"http://purl.org/rss/1.0/ title"`
Link string `xml:"link"` Link string `xml:"link"`
Description string `xml:"description"` Description string `xml:"description"`

View file

@ -19,15 +19,11 @@ import (
"miniflux.app/v2/internal/urllib" "miniflux.app/v2/internal/urllib"
) )
type RSSAdapter struct { type rssAdapter struct {
rss *RSS rss *rss
} }
func NewRSSAdapter(rss *RSS) *RSSAdapter { func (r *rssAdapter) buildFeed(baseURL string) *model.Feed {
return &RSSAdapter{rss}
}
func (r *RSSAdapter) BuildFeed(baseURL string) *model.Feed {
feed := &model.Feed{ feed := &model.Feed{
Title: html.UnescapeString(strings.TrimSpace(r.rss.Channel.Title)), Title: html.UnescapeString(strings.TrimSpace(r.rss.Channel.Title)),
FeedURL: strings.TrimSpace(baseURL), FeedURL: strings.TrimSpace(baseURL),
@ -145,7 +141,7 @@ func (r *RSSAdapter) BuildFeed(baseURL string) *model.Feed {
return feed return feed
} }
func findFeedAuthor(rssChannel *RSSChannel) string { func findFeedAuthor(rssChannel *rssChannel) string {
var author string var author string
switch { switch {
case rssChannel.ItunesAuthor != "": case rssChannel.ItunesAuthor != "":
@ -165,7 +161,7 @@ func findFeedAuthor(rssChannel *RSSChannel) string {
return strings.TrimSpace(sanitizer.StripTags(author)) return strings.TrimSpace(sanitizer.StripTags(author))
} }
func findFeedTags(rssChannel *RSSChannel) []string { func findFeedTags(rssChannel *rssChannel) []string {
tags := make([]string, 0) tags := make([]string, 0)
for _, tag := range rssChannel.Categories { for _, tag := range rssChannel.Categories {
@ -189,7 +185,7 @@ func findFeedTags(rssChannel *RSSChannel) []string {
return tags return tags
} }
func findEntryTitle(rssItem *RSSItem) string { func findEntryTitle(rssItem *rssItem) string {
title := rssItem.Title.Content title := rssItem.Title.Content
if rssItem.DublinCoreTitle != "" { if rssItem.DublinCoreTitle != "" {
@ -199,7 +195,7 @@ func findEntryTitle(rssItem *RSSItem) string {
return html.UnescapeString(html.UnescapeString(strings.TrimSpace(title))) return html.UnescapeString(html.UnescapeString(strings.TrimSpace(title)))
} }
func findEntryURL(rssItem *RSSItem) string { func findEntryURL(rssItem *rssItem) string {
for _, link := range []string{rssItem.FeedBurnerLink, rssItem.Link} { for _, link := range []string{rssItem.FeedBurnerLink, rssItem.Link} {
if link != "" { if link != "" {
return strings.TrimSpace(link) return strings.TrimSpace(link)
@ -222,7 +218,7 @@ func findEntryURL(rssItem *RSSItem) string {
return "" return ""
} }
func findEntryContent(rssItem *RSSItem) string { func findEntryContent(rssItem *rssItem) string {
for _, value := range []string{ for _, value := range []string{
rssItem.DublinCoreContent, rssItem.DublinCoreContent,
rssItem.Description, rssItem.Description,
@ -237,7 +233,7 @@ func findEntryContent(rssItem *RSSItem) string {
return "" return ""
} }
func findEntryDate(rssItem *RSSItem) time.Time { func findEntryDate(rssItem *rssItem) time.Time {
value := rssItem.PubDate value := rssItem.PubDate
if rssItem.DublinCoreDate != "" { if rssItem.DublinCoreDate != "" {
value = rssItem.DublinCoreDate value = rssItem.DublinCoreDate
@ -260,7 +256,7 @@ func findEntryDate(rssItem *RSSItem) time.Time {
return time.Now() return time.Now()
} }
func findEntryAuthor(rssItem *RSSItem) string { func findEntryAuthor(rssItem *rssItem) string {
var author string var author string
switch { switch {
@ -283,7 +279,7 @@ func findEntryAuthor(rssItem *RSSItem) string {
return strings.TrimSpace(sanitizer.StripTags(author)) return strings.TrimSpace(sanitizer.StripTags(author))
} }
func findEntryTags(rssItem *RSSItem) []string { func findEntryTags(rssItem *rssItem) []string {
tags := make([]string, 0) tags := make([]string, 0)
for _, tag := range rssItem.Categories { for _, tag := range rssItem.Categories {
@ -303,7 +299,7 @@ func findEntryTags(rssItem *RSSItem) []string {
return tags return tags
} }
func findEntryEnclosures(rssItem *RSSItem, siteURL string) model.EnclosureList { func findEntryEnclosures(rssItem *rssItem, siteURL string) model.EnclosureList {
enclosures := make(model.EnclosureList, 0) enclosures := make(model.EnclosureList, 0)
duplicates := make(map[string]bool) duplicates := make(map[string]bool)

View file

@ -7,14 +7,14 @@ import (
"miniflux.app/v2/internal/reader/atom" "miniflux.app/v2/internal/reader/atom"
) )
type AtomAuthor struct { type atomAuthor struct {
Author atom.AtomPerson `xml:"http://www.w3.org/2005/Atom author"` Author atom.AtomPerson `xml:"http://www.w3.org/2005/Atom author"`
} }
func (a *AtomAuthor) PersonName() string { func (a *atomAuthor) PersonName() string {
return a.Author.PersonName() return a.Author.PersonName()
} }
type AtomLinks struct { type atomLinks struct {
Links []*atom.AtomLink `xml:"http://www.w3.org/2005/Atom link"` Links []*atom.AtomLink `xml:"http://www.w3.org/2005/Atom link"`
} }

View file

@ -3,8 +3,8 @@
package rss // import "miniflux.app/v2/internal/reader/rss" package rss // import "miniflux.app/v2/internal/reader/rss"
// FeedBurnerItemElement represents FeedBurner XML elements. // feedBurnerItemElement represents FeedBurner XML elements.
type FeedBurnerItemElement struct { type feedBurnerItemElement struct {
FeedBurnerLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"` FeedBurnerLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"`
FeedBurnerEnclosureLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"` FeedBurnerEnclosureLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"`
} }

View file

@ -13,11 +13,12 @@ import (
// Parse returns a normalized feed struct from a RSS feed. // Parse returns a normalized feed struct from a RSS feed.
func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) { func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) {
rssFeed := new(RSS) rssFeed := new(rss)
decoder := xml.NewXMLDecoder(data) decoder := xml.NewXMLDecoder(data)
decoder.DefaultSpace = "rss" decoder.DefaultSpace = "rss"
if err := decoder.Decode(rssFeed); err != nil { if err := decoder.Decode(rssFeed); err != nil {
return nil, fmt.Errorf("rss: unable to parse feed: %w", err) return nil, fmt.Errorf("rss: unable to parse feed: %w", err)
} }
return NewRSSAdapter(rssFeed).BuildFeed(baseURL), nil adapter := &rssAdapter{rssFeed}
return adapter.buildFeed(baseURL), nil
} }

View file

@ -10,20 +10,20 @@ import (
"strings" "strings"
) )
var ErrInvalidDurationFormat = errors.New("rss: invalid duration format") var errInvalidDurationFormat = errors.New("rss: invalid duration format")
func getDurationInMinutes(rawDuration string) (int, error) { func getDurationInMinutes(rawDuration string) (int, error) {
var sumSeconds int var sumSeconds int
durationParts := strings.Split(rawDuration, ":") durationParts := strings.Split(rawDuration, ":")
if len(durationParts) > 3 { if len(durationParts) > 3 {
return 0, ErrInvalidDurationFormat return 0, errInvalidDurationFormat
} }
for i, durationPart := range durationParts { for i, durationPart := range durationParts {
durationPartValue, err := strconv.Atoi(durationPart) durationPartValue, err := strconv.Atoi(durationPart)
if err != nil { if err != nil {
return 0, ErrInvalidDurationFormat return 0, errInvalidDurationFormat
} }
sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue

View file

@ -15,15 +15,15 @@ import (
) )
// Specs: https://www.rssboard.org/rss-specification // Specs: https://www.rssboard.org/rss-specification
type RSS struct { type rss struct {
// Version is the version of the RSS specification. // Version is the version of the RSS specification.
Version string `xml:"rss version,attr"` Version string `xml:"rss version,attr"`
// Channel is the main container for the RSS feed. // Channel is the main container for the RSS feed.
Channel RSSChannel `xml:"rss channel"` Channel rssChannel `xml:"rss channel"`
} }
type RSSChannel struct { type rssChannel struct {
// Title is the name of the channel. // Title is the name of the channel.
Title string `xml:"rss title"` Title string `xml:"rss title"`
@ -64,10 +64,10 @@ type RSSChannel struct {
DocumentationURL string `xml:"rss docs"` DocumentationURL string `xml:"rss docs"`
// Cloud is a web service that supports the rssCloud interface which can be implemented in HTTP-POST, XML-RPC or SOAP 1.1. // Cloud is a web service that supports the rssCloud interface which can be implemented in HTTP-POST, XML-RPC or SOAP 1.1.
Cloud *RSSCloud `xml:"rss cloud"` Cloud *rssCloud `xml:"rss cloud"`
// Image specifies a GIF, JPEG or PNG image that can be displayed with the channel. // Image specifies a GIF, JPEG or PNG image that can be displayed with the channel.
Image *RSSImage `xml:"rss image"` Image *rssImage `xml:"rss image"`
// TTL is a number of minutes that indicates how long a channel can be cached before refreshing from the source. // TTL is a number of minutes that indicates how long a channel can be cached before refreshing from the source.
TTL string `xml:"rss ttl"` TTL string `xml:"rss ttl"`
@ -83,14 +83,14 @@ type RSSChannel struct {
SkipDays []string `xml:"rss skipDays>day"` SkipDays []string `xml:"rss skipDays>day"`
// Items is a collection of items. // Items is a collection of items.
Items []RSSItem `xml:"rss item"` Items []rssItem `xml:"rss item"`
AtomLinks atomLinks
itunes.ItunesChannelElement itunes.ItunesChannelElement
googleplay.GooglePlayChannelElement googleplay.GooglePlayChannelElement
} }
type RSSCloud struct { type rssCloud struct {
Domain string `xml:"domain,attr"` Domain string `xml:"domain,attr"`
Port string `xml:"port,attr"` Port string `xml:"port,attr"`
Path string `xml:"path,attr"` Path string `xml:"path,attr"`
@ -98,7 +98,7 @@ type RSSCloud struct {
Protocol string `xml:"protocol,attr"` Protocol string `xml:"protocol,attr"`
} }
type RSSImage struct { type rssImage struct {
// URL is the URL of a GIF, JPEG or PNG image that represents the channel. // URL is the URL of a GIF, JPEG or PNG image that represents the channel.
URL string `xml:"url"` URL string `xml:"url"`
@ -109,9 +109,9 @@ type RSSImage struct {
Link string `xml:"link"` Link string `xml:"link"`
} }
type RSSItem struct { type rssItem struct {
// Title is the title of the item. // Title is the title of the item.
Title InnerContent `xml:"rss title"` Title innerContent `xml:"rss title"`
// Link is the URL of the item. // Link is the URL of the item.
Link string `xml:"rss link"` Link string `xml:"rss link"`
@ -120,7 +120,7 @@ type RSSItem struct {
Description string `xml:"rss description"` Description string `xml:"rss description"`
// Author is the email address of the author of the item. // Author is the email address of the author of the item.
Author RSSAuthor `xml:"rss author"` Author rssAuthor `xml:"rss author"`
// <category> is an optional sub-element of <item>. // <category> is an optional sub-element of <item>.
// It has one optional attribute, domain, a string that identifies a categorization taxonomy. // It has one optional attribute, domain, a string that identifies a categorization taxonomy.
@ -133,7 +133,7 @@ type RSSItem struct {
// <enclosure> is an optional sub-element of <item>. // <enclosure> is an optional sub-element of <item>.
// It has three required attributes. url says where the enclosure is located, // It has three required attributes. url says where the enclosure is located,
// length says how big it is in bytes, and type says what its type is, a standard MIME type. // length says how big it is in bytes, and type says what its type is, a standard MIME type.
Enclosures []RSSEnclosure `xml:"rss enclosure"` Enclosures []rssEnclosure `xml:"rss enclosure"`
// <guid> is an optional sub-element of <item>. // <guid> is an optional sub-element of <item>.
// It's a string that uniquely identifies the item. // It's a string that uniquely identifies the item.
@ -149,7 +149,7 @@ type RSSItem struct {
// //
// isPermaLink is optional, its default value is true. // isPermaLink is optional, its default value is true.
// If its value is false, the guid may not be assumed to be a url, or a url to anything in particular. // If its value is false, the guid may not be assumed to be a url, or a url to anything in particular.
GUID RSSGUID `xml:"rss guid"` GUID rssGUID `xml:"rss guid"`
// <pubDate> is the publication date of the item. // <pubDate> is the publication date of the item.
// Its value is a string in RFC 822 format. // Its value is a string in RFC 822 format.
@ -158,30 +158,30 @@ type RSSItem struct {
// <source> is an optional sub-element of <item>. // <source> is an optional sub-element of <item>.
// Its value is the name of the RSS channel that the item came from, derived from its <title>. // Its value is the name of the RSS channel that the item came from, derived from its <title>.
// It has one required attribute, url, which contains the URL of the RSS channel. // It has one required attribute, url, which contains the URL of the RSS channel.
Source RSSSource `xml:"rss source"` Source rssSource `xml:"rss source"`
dublincore.DublinCoreItemElement dublincore.DublinCoreItemElement
FeedBurnerItemElement feedBurnerItemElement
media.MediaItemElement media.MediaItemElement
AtomAuthor atomAuthor
AtomLinks atomLinks
itunes.ItunesItemElement itunes.ItunesItemElement
googleplay.GooglePlayItemElement googleplay.GooglePlayItemElement
} }
type RSSAuthor struct { type rssAuthor struct {
XMLName xml.Name XMLName xml.Name
Data string `xml:",chardata"` Data string `xml:",chardata"`
Inner string `xml:",innerxml"` Inner string `xml:",innerxml"`
} }
type RSSEnclosure struct { type rssEnclosure struct {
URL string `xml:"url,attr"` URL string `xml:"url,attr"`
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
Length string `xml:"length,attr"` Length string `xml:"length,attr"`
} }
func (enclosure *RSSEnclosure) Size() int64 { func (enclosure *rssEnclosure) Size() int64 {
if strings.TrimSpace(enclosure.Length) == "" { if strings.TrimSpace(enclosure.Length) == "" {
return 0 return 0
} }
@ -189,21 +189,21 @@ func (enclosure *RSSEnclosure) Size() int64 {
return size return size
} }
type RSSGUID struct { type rssGUID struct {
Data string `xml:",chardata"` Data string `xml:",chardata"`
IsPermaLink string `xml:"isPermaLink,attr"` IsPermaLink string `xml:"isPermaLink,attr"`
} }
type RSSSource struct { type rssSource struct {
URL string `xml:"url,attr"` URL string `xml:"url,attr"`
Name string `xml:",chardata"` Name string `xml:",chardata"`
} }
type InnerContent struct { type innerContent struct {
Content string Content string
} }
func (ic *InnerContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (ic *innerContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var content strings.Builder var content strings.Builder
for { for {

View file

@ -197,7 +197,8 @@ type SanitizerOptions struct {
OpenLinksInNewTab bool OpenLinksInNewTab bool
} }
func SanitizeHTMLWithDefaultOptions(baseURL, rawHTML string) string { // TODO: replace with SanitizeHTML, as it's only used in tests.
func sanitizeHTMLWithDefaultOptions(baseURL, rawHTML string) string {
return SanitizeHTML(baseURL, rawHTML, &SanitizerOptions{ return SanitizeHTML(baseURL, rawHTML, &SanitizerOptions{
OpenLinksInNewTab: true, OpenLinksInNewTab: true,
}) })

View file

@ -27,7 +27,7 @@ func BenchmarkSanitize(b *testing.B) {
} }
for b.Loop() { for b.Loop() {
for _, v := range testCases { for _, v := range testCases {
SanitizeHTMLWithDefaultOptions(v[0], v[1]) sanitizeHTMLWithDefaultOptions(v[0], v[1])
} }
} }
} }
@ -40,7 +40,7 @@ func FuzzSanitizer(f *testing.F) {
i++ i++
} }
out := SanitizeHTMLWithDefaultOptions("", orig) out := sanitizeHTMLWithDefaultOptions("", orig)
tok = html.NewTokenizer(strings.NewReader(out)) tok = html.NewTokenizer(strings.NewReader(out))
j := 0 j := 0
@ -56,7 +56,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 := SanitizeHTMLWithDefaultOptions("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)
@ -66,7 +66,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 := SanitizeHTMLWithDefaultOptions("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)
@ -76,7 +76,7 @@ func TestImgWithWidthAndHeightAttribute(t *testing.T) {
func TestImgWithWidthAttributeLargerThanMinifluxLayout(t *testing.T) { func TestImgWithWidthAttributeLargerThanMinifluxLayout(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 := SanitizeHTMLWithDefaultOptions("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)
@ -86,7 +86,7 @@ func TestImgWithWidthAttributeLargerThanMinifluxLayout(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 := SanitizeHTMLWithDefaultOptions("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)
@ -96,7 +96,7 @@ func TestImgWithIncorrectWidthAndHeightAttribute(t *testing.T) {
func TestImgWithIncorrectWidthAttribute(t *testing.T) { func TestImgWithIncorrectWidthAttribute(t *testing.T) {
input := `<img src="https://example.org/image.png" width="10px" height="20">` input := `<img src="https://example.org/image.png" width="10px" height="20">`
expected := `<img src="https://example.org/image.png" height="20" loading="lazy">` expected := `<img src="https://example.org/image.png" height="20" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("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)
@ -106,7 +106,7 @@ func TestImgWithIncorrectWidthAttribute(t *testing.T) {
func TestImgWithEmptyWidthAndHeightAttribute(t *testing.T) { func TestImgWithEmptyWidthAndHeightAttribute(t *testing.T) {
input := `<img src="https://example.org/image.png" width="" height="">` input := `<img src="https://example.org/image.png" width="" height="">`
expected := `<img src="https://example.org/image.png" loading="lazy">` expected := `<img src="https://example.org/image.png" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("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)
@ -116,7 +116,7 @@ func TestImgWithEmptyWidthAndHeightAttribute(t *testing.T) {
func TestImgWithIncorrectHeightAttribute(t *testing.T) { func TestImgWithIncorrectHeightAttribute(t *testing.T) {
input := `<img src="https://example.org/image.png" width="10" height="20px">` input := `<img src="https://example.org/image.png" width="10" height="20px">`
expected := `<img src="https://example.org/image.png" width="10" loading="lazy">` expected := `<img src="https://example.org/image.png" width="10" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("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)
@ -126,7 +126,7 @@ func TestImgWithIncorrectHeightAttribute(t *testing.T) {
func TestImgWithNegativeWidthAttribute(t *testing.T) { func TestImgWithNegativeWidthAttribute(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" height="20" loading="lazy">` expected := `<img src="https://example.org/image.png" height="20" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("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)
@ -136,7 +136,7 @@ func TestImgWithNegativeWidthAttribute(t *testing.T) {
func TestImgWithNegativeHeightAttribute(t *testing.T) { func TestImgWithNegativeHeightAttribute(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" loading="lazy">` expected := `<img src="https://example.org/image.png" width="10" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("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)
@ -146,7 +146,7 @@ func TestImgWithNegativeHeightAttribute(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 := SanitizeHTMLWithDefaultOptions("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)
@ -156,7 +156,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 := SanitizeHTMLWithDefaultOptions("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)
@ -166,7 +166,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 := SanitizeHTMLWithDefaultOptions("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)
@ -176,7 +176,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 := SanitizeHTMLWithDefaultOptions("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)
@ -203,7 +203,7 @@ func TestImgWithFetchPriorityAttribute(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
output := SanitizeHTMLWithDefaultOptions("http://example.org/", tc.input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", tc.input)
if output != tc.expected { if output != tc.expected {
t.Errorf(`Wrong output for input %q: expected %q, got %q`, tc.input, tc.expected, output) t.Errorf(`Wrong output for input %q: expected %q, got %q`, tc.input, tc.expected, output)
} }
@ -213,7 +213,7 @@ func TestImgWithFetchPriorityAttribute(t *testing.T) {
func TestImgWithInvalidFetchPriorityAttribute(t *testing.T) { func TestImgWithInvalidFetchPriorityAttribute(t *testing.T) {
input := `<img src="https://example.org/image.png" fetchpriority="invalid">` input := `<img src="https://example.org/image.png" fetchpriority="invalid">`
expected := `<img src="https://example.org/image.png" loading="lazy">` expected := `<img src="https://example.org/image.png" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected { if output != expected {
t.Errorf(`Wrong output: expected %q, got %q`, expected, output) t.Errorf(`Wrong output: expected %q, got %q`, expected, output)
@ -223,7 +223,7 @@ func TestImgWithInvalidFetchPriorityAttribute(t *testing.T) {
func TestNonImgWithFetchPriorityAttribute(t *testing.T) { func TestNonImgWithFetchPriorityAttribute(t *testing.T) {
input := `<p fetchpriority="high">Text</p>` input := `<p fetchpriority="high">Text</p>`
expected := `<p>Text</p>` expected := `<p>Text</p>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected { if output != expected {
t.Errorf(`Wrong output: expected %q, got %q`, expected, output) t.Errorf(`Wrong output: expected %q, got %q`, expected, output)
@ -250,7 +250,7 @@ func TestImgWithDecodingAttribute(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
output := SanitizeHTMLWithDefaultOptions("http://example.org/", tc.input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", tc.input)
if output != tc.expected { if output != tc.expected {
t.Errorf(`Wrong output for input %q: expected %q, got %q`, tc.input, tc.expected, output) t.Errorf(`Wrong output for input %q: expected %q, got %q`, tc.input, tc.expected, output)
} }
@ -260,7 +260,7 @@ func TestImgWithDecodingAttribute(t *testing.T) {
func TestImgWithInvalidDecodingAttribute(t *testing.T) { func TestImgWithInvalidDecodingAttribute(t *testing.T) {
input := `<img src="https://example.org/image.png" decoding="invalid">` input := `<img src="https://example.org/image.png" decoding="invalid">`
expected := `<img src="https://example.org/image.png" loading="lazy">` expected := `<img src="https://example.org/image.png" loading="lazy">`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected { if output != expected {
t.Errorf(`Wrong output: expected %q, got %q`, expected, output) t.Errorf(`Wrong output: expected %q, got %q`, expected, output)
@ -270,7 +270,7 @@ func TestImgWithInvalidDecodingAttribute(t *testing.T) {
func TestNonImgWithDecodingAttribute(t *testing.T) { func TestNonImgWithDecodingAttribute(t *testing.T) {
input := `<p decoding="async">Text</p>` input := `<p decoding="async">Text</p>`
expected := `<p>Text</p>` expected := `<p>Text</p>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if output != expected { if output != expected {
t.Errorf(`Wrong output: expected %q, got %q`, expected, output) t.Errorf(`Wrong output: expected %q, got %q`, expected, output)
@ -280,7 +280,7 @@ func TestNonImgWithDecodingAttribute(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 := SanitizeHTMLWithDefaultOptions("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)
@ -290,7 +290,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 := SanitizeHTMLWithDefaultOptions("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)
@ -299,7 +299,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 := SanitizeHTMLWithDefaultOptions("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)
@ -308,7 +308,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 := SanitizeHTMLWithDefaultOptions("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)
@ -318,7 +318,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -328,7 +328,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -338,7 +338,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 := SanitizeHTMLWithDefaultOptions("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)
@ -348,7 +348,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 := SanitizeHTMLWithDefaultOptions("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)
@ -358,7 +358,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 := SanitizeHTMLWithDefaultOptions("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)
@ -368,7 +368,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 := SanitizeHTMLWithDefaultOptions("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)
@ -378,7 +378,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 := SanitizeHTMLWithDefaultOptions("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)
@ -390,7 +390,7 @@ func TestInvalidIFrame(t *testing.T) {
input := `<iframe src="http://example.org/"></iframe>` input := `<iframe src="http://example.org/"></iframe>`
expected := `` expected := ``
output := SanitizeHTMLWithDefaultOptions("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)
@ -402,7 +402,7 @@ func TestSameDomainIFrame(t *testing.T) {
input := `<iframe src="http://example.com/test"></iframe>` input := `<iframe src="http://example.com/test"></iframe>`
expected := `` expected := ``
output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) output := sanitizeHTMLWithDefaultOptions("http://example.com/", input)
if expected != output { if expected != output {
t.Errorf(`Wrong output: %q != %q`, expected, output) t.Errorf(`Wrong output: %q != %q`, expected, output)
@ -414,7 +414,7 @@ func TestInvidiousIFrame(t *testing.T) {
input := `<iframe src="https://yewtu.be/watch?v=video_id"></iframe>` input := `<iframe src="https://yewtu.be/watch?v=video_id"></iframe>`
expected := `<iframe src="https://yewtu.be/watch?v=video_id" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>` expected := `<iframe src="https://yewtu.be/watch?v=video_id" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) output := sanitizeHTMLWithDefaultOptions("http://example.com/", input)
if expected != output { if expected != output {
t.Errorf(`Wrong output: %q != %q`, expected, output) t.Errorf(`Wrong output: %q != %q`, expected, output)
@ -432,7 +432,7 @@ func TestCustomYoutubeEmbedURL(t *testing.T) {
input := `<iframe src="https://www.invidious.custom/embed/1234"></iframe>` input := `<iframe src="https://www.invidious.custom/embed/1234"></iframe>`
expected := `<iframe src="https://www.invidious.custom/embed/1234" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>` expected := `<iframe src="https://www.invidious.custom/embed/1234" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("http://example.com/", input) output := sanitizeHTMLWithDefaultOptions("http://example.com/", input)
if expected != output { if expected != output {
t.Errorf(`Wrong output: %q != %q`, expected, output) t.Errorf(`Wrong output: %q != %q`, expected, output)
@ -444,7 +444,7 @@ 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 := SanitizeHTMLWithDefaultOptions("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)
@ -474,7 +474,7 @@ func TestLinkWithNoTarget(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 := SanitizeHTMLWithDefaultOptions("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)
@ -484,7 +484,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 := SanitizeHTMLWithDefaultOptions("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)
@ -494,7 +494,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -504,7 +504,7 @@ 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" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected := `<p>This link is <a href="bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) 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)
@ -514,7 +514,7 @@ 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" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected := `<p>This link is <a href="callto:12345679" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) 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)
@ -524,7 +524,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -532,7 +532,7 @@ func TestFeedURIScheme(t *testing.T) {
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" referrerpolicy="no-referrer" target="_blank">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 = SanitizeHTMLWithDefaultOptions("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)
@ -542,7 +542,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -552,7 +552,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -560,7 +560,7 @@ func TestItunesURIScheme(t *testing.T) {
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" referrerpolicy="no-referrer" target="_blank">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 = SanitizeHTMLWithDefaultOptions("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)
@ -570,7 +570,7 @@ 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&amp;xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7">valid</a></p>` input := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&amp;xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7">valid</a></p>`
expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&amp;xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&amp;xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -580,7 +580,7 @@ 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&amp;body=My%20idea%20is%3A%20%0A">valid</a></p>` input := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&amp;body=My%20idea%20is%3A%20%0A">valid</a></p>`
expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&amp;body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&amp;body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -590,7 +590,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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 +598,7 @@ func TestNewsURIScheme(t *testing.T) {
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" referrerpolicy="no-referrer" target="_blank">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 = SanitizeHTMLWithDefaultOptions("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)
@ -606,7 +606,7 @@ func TestNewsURIScheme(t *testing.T) {
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" referrerpolicy="no-referrer" target="_blank">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 = SanitizeHTMLWithDefaultOptions("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)
@ -616,7 +616,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -626,7 +626,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -634,7 +634,7 @@ func TestSIPURIScheme(t *testing.T) {
input = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&amp;priority=urgent">valid</a></p>` input = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&amp;priority=urgent">valid</a></p>`
expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&amp;priority=urgent" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&amp;priority=urgent" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output = SanitizeHTMLWithDefaultOptions("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)
@ -644,7 +644,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -654,7 +654,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -664,7 +664,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -674,7 +674,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -682,7 +682,7 @@ func TestSubversionURIScheme(t *testing.T) {
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" referrerpolicy="no-referrer" target="_blank">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 = SanitizeHTMLWithDefaultOptions("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)
@ -692,7 +692,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -702,7 +702,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -712,7 +712,7 @@ 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&amp;type=subscribed">valid</a></p>` input := `<p>This link is <a href="xmpp:user@host?subscribe&amp;type=subscribed">valid</a></p>`
expected := `<p>This link is <a href="xmpp:user@host?subscribe&amp;type=subscribed" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>` expected := `<p>This link is <a href="xmpp:user@host?subscribe&amp;type=subscribed" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -722,7 +722,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 := SanitizeHTMLWithDefaultOptions("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)
@ -732,7 +732,7 @@ 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" referrerpolicy="no-referrer" target="_blank">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 := SanitizeHTMLWithDefaultOptions("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)
@ -742,7 +742,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 := SanitizeHTMLWithDefaultOptions("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)
@ -752,7 +752,7 @@ func TestImageSrcWithTrackers(t *testing.T) {
func Test1x1PixelTracker(t *testing.T) { func Test1x1PixelTracker(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 := SanitizeHTMLWithDefaultOptions("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)
@ -762,7 +762,7 @@ func Test1x1PixelTracker(t *testing.T) {
func Test0x0PixelTracker(t *testing.T) { func Test0x0PixelTracker(t *testing.T) {
input := `<p><img src="https://tracker1.example.org/" height="0" width="0"> and <img src="https://tracker2.example.org/" height="0" width="0"/></p>` input := `<p><img src="https://tracker1.example.org/" height="0" width="0"> and <img src="https://tracker2.example.org/" height="0" width="0"/></p>`
expected := `<p> and </p>` expected := `<p> and </p>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -772,7 +772,7 @@ func Test0x0PixelTracker(t *testing.T) {
func TestXmlEntities(t *testing.T) { func TestXmlEntities(t *testing.T) {
input := `<pre>echo "test" &gt; /etc/hosts</pre>` input := `<pre>echo "test" &gt; /etc/hosts</pre>`
expected := `<pre>echo &#34;test&#34; &gt; /etc/hosts</pre>` expected := `<pre>echo &#34;test&#34; &gt; /etc/hosts</pre>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -782,7 +782,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="&lt;b&gt;test&lt;/b&gt;">test</td>` expected := `<td rowspan="&lt;b&gt;test&lt;/b&gt;">test</td>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -792,7 +792,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&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>` input := `<iframe src="http://www.youtube.com/embed/test123?version=3&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;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&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -802,7 +802,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 := SanitizeHTMLWithDefaultOptions("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)
@ -812,7 +812,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&amp;controls=0"></iframe>` input := `<iframe src="https://www.youtube.com/embed/test123?rel=0&amp;controls=0"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;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&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -822,7 +822,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&amp;controls=0" sandbox="allow-scripts allow-same-origin"></iframe>` input := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&amp;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&amp;controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -832,7 +832,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 := SanitizeHTMLWithDefaultOptions("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)
@ -851,7 +851,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
input := `<iframe src="https://www.youtube.com/embed/test123?version=3&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>` input := `<iframe src="https://www.youtube.com/embed/test123?version=3&#038;rel=1&#038;fs=1&#038;autohide=2&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;wmode=transparent"></iframe>`
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;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&amp;rel=1&amp;fs=1&amp;autohide=2&amp;showsearch=0&amp;showinfo=1&amp;iv_load_policy=1&amp;wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -861,7 +861,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
func TestVimeoIframeRewriteWithQueryString(t *testing.T) { func TestVimeoIframeRewriteWithQueryString(t *testing.T) {
input := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0"></iframe>` input := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0"></iframe>`
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&amp;byline=0&amp;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&amp;byline=0&amp;dnt=1" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output { if expected != output {
t.Errorf(`Wrong output: %q != %q`, expected, output) t.Errorf(`Wrong output: %q != %q`, expected, output)
@ -871,7 +871,7 @@ func TestVimeoIframeRewriteWithQueryString(t *testing.T) {
func TestVimeoIframeRewriteWithoutQueryString(t *testing.T) { func TestVimeoIframeRewriteWithoutQueryString(t *testing.T) {
input := `<iframe src="https://player.vimeo.com/video/123456"></iframe>` input := `<iframe src="https://player.vimeo.com/video/123456"></iframe>`
expected := `<iframe src="https://player.vimeo.com/video/123456?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?dnt=1" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
output := SanitizeHTMLWithDefaultOptions("http://example.org/", input) output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
if expected != output { if expected != output {
t.Errorf(`Wrong output: %q != %q`, expected, output) t.Errorf(`Wrong output: %q != %q`, expected, output)
@ -881,7 +881,7 @@ func TestVimeoIframeRewriteWithoutQueryString(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 := SanitizeHTMLWithDefaultOptions("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)
@ -891,7 +891,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 := SanitizeHTMLWithDefaultOptions("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)
@ -901,7 +901,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 := SanitizeHTMLWithDefaultOptions("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)
@ -911,7 +911,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 := SanitizeHTMLWithDefaultOptions("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)
@ -922,7 +922,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 := SanitizeHTMLWithDefaultOptions("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)
} }
@ -931,7 +931,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 := SanitizeHTMLWithDefaultOptions("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)
@ -941,7 +941,7 @@ func TestMathML(t *testing.T) {
func TestInvalidMathMLXMLNamespace(t *testing.T) { func TestInvalidMathMLXMLNamespace(t *testing.T) {
input := `<math xmlns="http://example.org"><msup><mi>x</mi><mn>2</mn></msup></math>` input := `<math xmlns="http://example.org"><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 := SanitizeHTMLWithDefaultOptions("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)
@ -951,7 +951,7 @@ func TestInvalidMathMLXMLNamespace(t *testing.T) {
func TestBlockedResourcesSubstrings(t *testing.T) { func TestBlockedResourcesSubstrings(t *testing.T) {
input := `<p>Before paragraph.</p><img src="http://stats.wordpress.com/something.php" alt="Blocked Resource"><p>After paragraph.</p>` input := `<p>Before paragraph.</p><img src="http://stats.wordpress.com/something.php" alt="Blocked Resource"><p>After paragraph.</p>`
expected := `<p>Before paragraph.</p><p>After paragraph.</p>` expected := `<p>Before paragraph.</p><p>After paragraph.</p>`
output := SanitizeHTMLWithDefaultOptions("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)
@ -959,7 +959,7 @@ func TestBlockedResourcesSubstrings(t *testing.T) {
input = `<p>Before paragraph.</p><img src="http://twitter.com/share?text=This+is+google+a+search+engine&url=https%3A%2F%2Fwww.google.com" alt="Blocked Resource"><p>After paragraph.</p>` input = `<p>Before paragraph.</p><img src="http://twitter.com/share?text=This+is+google+a+search+engine&url=https%3A%2F%2Fwww.google.com" alt="Blocked Resource"><p>After paragraph.</p>`
expected = `<p>Before paragraph.</p><p>After paragraph.</p>` expected = `<p>Before paragraph.</p><p>After paragraph.</p>`
output = SanitizeHTMLWithDefaultOptions("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)
@ -967,7 +967,7 @@ func TestBlockedResourcesSubstrings(t *testing.T) {
input = `<p>Before paragraph.</p><img src="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.google.com%[title]=This+Is%2C+Google+a+search+engine" alt="Blocked Resource"><p>After paragraph.</p>` input = `<p>Before paragraph.</p><img src="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.google.com%[title]=This+Is%2C+Google+a+search+engine" alt="Blocked Resource"><p>After paragraph.</p>`
expected = `<p>Before paragraph.</p><p>After paragraph.</p>` expected = `<p>Before paragraph.</p><p>After paragraph.</p>`
output = SanitizeHTMLWithDefaultOptions("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)

View file

@ -9,14 +9,14 @@ import (
"strings" "strings"
) )
type ImageCandidate struct { type imageCandidate struct {
ImageURL string ImageURL string
Descriptor string Descriptor string
} }
type ImageCandidates []*ImageCandidate type imageCandidates []*imageCandidate
func (c ImageCandidates) String() string { func (c imageCandidates) String() string {
htmlCandidates := make([]string, 0, len(c)) htmlCandidates := make([]string, 0, len(c))
for _, imageCandidate := range c { for _, imageCandidate := range c {
@ -35,7 +35,7 @@ func (c ImageCandidates) String() string {
// ParseSrcSetAttribute returns the list of image candidates from the set. // ParseSrcSetAttribute returns the list of image candidates from the set.
// https://html.spec.whatwg.org/#parse-a-srcset-attribute // https://html.spec.whatwg.org/#parse-a-srcset-attribute
func ParseSrcSetAttribute(attributeValue string) (imageCandidates ImageCandidates) { func ParseSrcSetAttribute(attributeValue string) (imageCandidates imageCandidates) {
for _, unparsedCandidate := range strings.Split(attributeValue, ", ") { for _, unparsedCandidate := range strings.Split(attributeValue, ", ") {
if candidate, err := parseImageCandidate(unparsedCandidate); err == nil { if candidate, err := parseImageCandidate(unparsedCandidate); err == nil {
imageCandidates = append(imageCandidates, candidate) imageCandidates = append(imageCandidates, candidate)
@ -45,18 +45,18 @@ func ParseSrcSetAttribute(attributeValue string) (imageCandidates ImageCandidate
return imageCandidates return imageCandidates
} }
func parseImageCandidate(input string) (*ImageCandidate, error) { func parseImageCandidate(input string) (*imageCandidate, error) {
parts := strings.Split(strings.TrimSpace(input), " ") parts := strings.Split(strings.TrimSpace(input), " ")
nbParts := len(parts) nbParts := len(parts)
switch nbParts { switch nbParts {
case 1: case 1:
return &ImageCandidate{ImageURL: parts[0]}, nil return &imageCandidate{ImageURL: parts[0]}, nil
case 2: case 2:
if !isValidWidthOrDensityDescriptor(parts[1]) { if !isValidWidthOrDensityDescriptor(parts[1]) {
return nil, fmt.Errorf(`srcset: invalid descriptor`) return nil, fmt.Errorf(`srcset: invalid descriptor`)
} }
return &ImageCandidate{ImageURL: parts[0], Descriptor: parts[1]}, nil return &imageCandidate{ImageURL: parts[0], Descriptor: parts[1]}, nil
default: default:
return nil, fmt.Errorf(`srcset: invalid number of descriptors`) return nil, fmt.Errorf(`srcset: invalid number of descriptors`)
} }

View file

@ -22,27 +22,27 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
) )
type SubscriptionFinder struct { type subscriptionFinder struct {
requestBuilder *fetcher.RequestBuilder requestBuilder *fetcher.RequestBuilder
feedDownloaded bool feedDownloaded bool
feedResponseInfo *model.FeedCreationRequestFromSubscriptionDiscovery feedResponseInfo *model.FeedCreationRequestFromSubscriptionDiscovery
} }
func NewSubscriptionFinder(requestBuilder *fetcher.RequestBuilder) *SubscriptionFinder { func NewSubscriptionFinder(requestBuilder *fetcher.RequestBuilder) *subscriptionFinder {
return &SubscriptionFinder{ return &subscriptionFinder{
requestBuilder: requestBuilder, requestBuilder: requestBuilder,
} }
} }
func (f *SubscriptionFinder) IsFeedAlreadyDownloaded() bool { func (f *subscriptionFinder) IsFeedAlreadyDownloaded() bool {
return f.feedDownloaded return f.feedDownloaded
} }
func (f *SubscriptionFinder) FeedResponseInfo() *model.FeedCreationRequestFromSubscriptionDiscovery { func (f *subscriptionFinder) FeedResponseInfo() *model.FeedCreationRequestFromSubscriptionDiscovery {
return f.feedResponseInfo return f.feedResponseInfo
} }
func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string, rssBridgeToken string) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string, rssBridgeToken string) (Subscriptions, *locale.LocalizedErrorWrapper) {
responseHandler := fetcher.NewResponseHandler(f.requestBuilder.ExecuteRequest(websiteURL)) responseHandler := fetcher.NewResponseHandler(f.requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close() defer responseHandler.Close()
@ -71,7 +71,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
// Step 2) Check if the website URL is a YouTube channel. // Step 2) Check if the website URL is a YouTube channel.
slog.Debug("Try to detect feeds from YouTube channel page", slog.String("website_url", websiteURL)) slog.Debug("Try to detect feeds from YouTube channel page", slog.String("website_url", websiteURL))
if subscriptions, localizedError := f.FindSubscriptionsFromYouTubeChannelPage(websiteURL); localizedError != nil { if subscriptions, localizedError := f.findSubscriptionsFromYouTubeChannelPage(websiteURL); localizedError != nil {
return nil, localizedError return nil, localizedError
} else if len(subscriptions) > 0 { } else if len(subscriptions) > 0 {
slog.Debug("Subscriptions found from YouTube channel page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions)) slog.Debug("Subscriptions found from YouTube channel page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions))
@ -80,7 +80,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
// Step 3) Check if the website URL is a YouTube playlist. // Step 3) Check if the website URL is a YouTube playlist.
slog.Debug("Try to detect feeds from YouTube playlist page", slog.String("website_url", websiteURL)) slog.Debug("Try to detect feeds from YouTube playlist page", slog.String("website_url", websiteURL))
if subscriptions, localizedError := f.FindSubscriptionsFromYouTubePlaylistPage(websiteURL); localizedError != nil { if subscriptions, localizedError := f.findSubscriptionsFromYouTubePlaylistPage(websiteURL); localizedError != nil {
return nil, localizedError return nil, localizedError
} else if len(subscriptions) > 0 { } else if len(subscriptions) > 0 {
slog.Debug("Subscriptions found from YouTube playlist page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions)) slog.Debug("Subscriptions found from YouTube playlist page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions))
@ -92,7 +92,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
slog.String("website_url", websiteURL), slog.String("website_url", websiteURL),
slog.String("content_type", responseHandler.ContentType()), slog.String("content_type", responseHandler.ContentType()),
) )
if subscriptions, localizedError := f.FindSubscriptionsFromWebPage(websiteURL, responseHandler.ContentType(), bytes.NewReader(responseBody)); localizedError != nil { if subscriptions, localizedError := f.findSubscriptionsFromWebPage(websiteURL, responseHandler.ContentType(), bytes.NewReader(responseBody)); localizedError != nil {
return nil, localizedError return nil, localizedError
} else if len(subscriptions) > 0 { } else if len(subscriptions) > 0 {
slog.Debug("Subscriptions found from web page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions)) slog.Debug("Subscriptions found from web page", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions))
@ -102,7 +102,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
// Step 5) Check if the website URL can use RSS-Bridge. // Step 5) Check if the website URL can use RSS-Bridge.
if rssBridgeURL != "" { if rssBridgeURL != "" {
slog.Debug("Try to detect feeds with RSS-Bridge", slog.String("website_url", websiteURL)) slog.Debug("Try to detect feeds with RSS-Bridge", slog.String("website_url", websiteURL))
if subscriptions, localizedError := f.FindSubscriptionsFromRSSBridge(websiteURL, rssBridgeURL, rssBridgeToken); localizedError != nil { if subscriptions, localizedError := f.findSubscriptionsFromRSSBridge(websiteURL, rssBridgeURL, rssBridgeToken); localizedError != nil {
return nil, localizedError return nil, localizedError
} else if len(subscriptions) > 0 { } else if len(subscriptions) > 0 {
slog.Debug("Subscriptions found from RSS-Bridge", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions)) slog.Debug("Subscriptions found from RSS-Bridge", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions))
@ -112,7 +112,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
// Step 6) Check if the website has a known feed URL. // Step 6) Check if the website has a known feed URL.
slog.Debug("Try to detect feeds from well-known URLs", slog.String("website_url", websiteURL)) slog.Debug("Try to detect feeds from well-known URLs", slog.String("website_url", websiteURL))
if subscriptions, localizedError := f.FindSubscriptionsFromWellKnownURLs(websiteURL); localizedError != nil { if subscriptions, localizedError := f.findSubscriptionsFromWellKnownURLs(websiteURL); localizedError != nil {
return nil, localizedError return nil, localizedError
} else if len(subscriptions) > 0 { } else if len(subscriptions) > 0 {
slog.Debug("Subscriptions found with well-known URLs", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions)) slog.Debug("Subscriptions found with well-known URLs", slog.String("website_url", websiteURL), slog.Any("subscriptions", subscriptions))
@ -122,7 +122,7 @@ func (f *SubscriptionFinder) FindSubscriptions(websiteURL, rssBridgeURL string,
return nil, nil return nil, nil
} }
func (f *SubscriptionFinder) FindSubscriptionsFromWebPage(websiteURL, contentType string, body io.Reader) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) findSubscriptionsFromWebPage(websiteURL, contentType string, body io.Reader) (Subscriptions, *locale.LocalizedErrorWrapper) {
queries := map[string]string{ queries := map[string]string{
"link[type='application/rss+xml']": parser.FormatRSS, "link[type='application/rss+xml']": parser.FormatRSS,
"link[type='application/atom+xml']": parser.FormatAtom, "link[type='application/atom+xml']": parser.FormatAtom,
@ -151,7 +151,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromWebPage(websiteURL, contentTyp
subscriptionURLs := make(map[string]bool) subscriptionURLs := make(map[string]bool)
for query, kind := range queries { for query, kind := range queries {
doc.Find(query).Each(func(i int, s *goquery.Selection) { doc.Find(query).Each(func(i int, s *goquery.Selection) {
subscription := new(Subscription) subscription := new(subscription)
subscription.Type = kind subscription.Type = kind
if title, exists := s.Attr("title"); exists { if title, exists := s.Attr("title"); exists {
@ -181,7 +181,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromWebPage(websiteURL, contentTyp
return subscriptions, nil return subscriptions, nil
} }
func (f *SubscriptionFinder) FindSubscriptionsFromWellKnownURLs(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) findSubscriptionsFromWellKnownURLs(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) {
knownURLs := map[string]string{ knownURLs := map[string]string{
"atom.xml": parser.FormatAtom, "atom.xml": parser.FormatAtom,
"feed.atom": parser.FormatAtom, "feed.atom": parser.FormatAtom,
@ -237,7 +237,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromWellKnownURLs(websiteURL strin
continue continue
} }
subscriptions = append(subscriptions, &Subscription{ subscriptions = append(subscriptions, &subscription{
Type: kind, Type: kind,
Title: fullURL, Title: fullURL,
URL: fullURL, URL: fullURL,
@ -248,7 +248,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromWellKnownURLs(websiteURL strin
return subscriptions, nil return subscriptions, nil
} }
func (f *SubscriptionFinder) FindSubscriptionsFromRSSBridge(websiteURL, rssBridgeURL string, rssBridgeToken string) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) findSubscriptionsFromRSSBridge(websiteURL, rssBridgeURL string, rssBridgeToken string) (Subscriptions, *locale.LocalizedErrorWrapper) {
slog.Debug("Trying to detect feeds using RSS-Bridge", slog.Debug("Trying to detect feeds using RSS-Bridge",
slog.String("website_url", websiteURL), slog.String("website_url", websiteURL),
slog.String("rssbridge_url", rssBridgeURL), slog.String("rssbridge_url", rssBridgeURL),
@ -273,7 +273,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromRSSBridge(websiteURL, rssBridg
subscriptions := make(Subscriptions, 0, len(bridges)) subscriptions := make(Subscriptions, 0, len(bridges))
for _, bridge := range bridges { for _, bridge := range bridges {
subscriptions = append(subscriptions, &Subscription{ subscriptions = append(subscriptions, &subscription{
Title: bridge.BridgeMeta.Name, Title: bridge.BridgeMeta.Name,
URL: bridge.URL, URL: bridge.URL,
Type: parser.FormatAtom, Type: parser.FormatAtom,
@ -283,7 +283,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromRSSBridge(websiteURL, rssBridg
return subscriptions, nil return subscriptions, nil
} }
func (f *SubscriptionFinder) FindSubscriptionsFromYouTubeChannelPage(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) findSubscriptionsFromYouTubeChannelPage(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) {
decodedUrl, err := url.Parse(websiteURL) decodedUrl, err := url.Parse(websiteURL)
if err != nil { if err != nil {
return nil, locale.NewLocalizedErrorWrapper(err, "error.invalid_site_url", err) return nil, locale.NewLocalizedErrorWrapper(err, "error.invalid_site_url", err)
@ -302,7 +302,7 @@ func (f *SubscriptionFinder) FindSubscriptionsFromYouTubeChannelPage(websiteURL
return nil, nil return nil, nil
} }
func (f *SubscriptionFinder) FindSubscriptionsFromYouTubePlaylistPage(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) { func (f *subscriptionFinder) findSubscriptionsFromYouTubePlaylistPage(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) {
decodedUrl, err := url.Parse(websiteURL) decodedUrl, err := url.Parse(websiteURL)
if err != nil { if err != nil {
return nil, locale.NewLocalizedErrorWrapper(err, "error.invalid_site_url", err) return nil, locale.NewLocalizedErrorWrapper(err, "error.invalid_site_url", err)

View file

@ -70,7 +70,7 @@ func TestFindYoutubePlaylistFeed(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
subscriptions, localizedError := NewSubscriptionFinder(nil).FindSubscriptionsFromYouTubePlaylistPage(scenario.websiteURL) subscriptions, localizedError := NewSubscriptionFinder(nil).findSubscriptionsFromYouTubePlaylistPage(scenario.websiteURL)
if scenario.discoveryError { if scenario.discoveryError {
if localizedError == nil { if localizedError == nil {
t.Fatalf(`Parsing an invalid URL should return an error`) t.Fatalf(`Parsing an invalid URL should return an error`)
@ -159,7 +159,7 @@ func TestFindYoutubeChannelFeed(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
subscriptions, localizedError := NewSubscriptionFinder(nil).FindSubscriptionsFromYouTubeChannelPage(scenario.websiteURL) subscriptions, localizedError := NewSubscriptionFinder(nil).findSubscriptionsFromYouTubeChannelPage(scenario.websiteURL)
if scenario.discoveryError { if scenario.discoveryError {
if localizedError == nil { if localizedError == nil {
t.Fatalf(`Parsing an invalid URL should return an error`) t.Fatalf(`Parsing an invalid URL should return an error`)
@ -197,7 +197,7 @@ func TestParseWebPageWithRssFeed(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -230,7 +230,7 @@ func TestParseWebPageWithAtomFeed(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -263,7 +263,7 @@ func TestParseWebPageWithJSONFeed(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -296,7 +296,7 @@ func TestParseWebPageWithOldJSONFeedMimeType(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -329,7 +329,7 @@ func TestParseWebPageWithRelativeFeedURL(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -362,7 +362,7 @@ func TestParseWebPageWithEmptyTitle(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -396,7 +396,7 @@ func TestParseWebPageWithMultipleFeeds(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -418,7 +418,7 @@ func TestParseWebPageWithDuplicatedFeeds(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -451,7 +451,7 @@ func TestParseWebPageWithEmptyFeedURL(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }
@ -472,7 +472,7 @@ func TestParseWebPageWithNoHref(t *testing.T) {
</body> </body>
</html>` </html>`
subscriptions, err := NewSubscriptionFinder(nil).FindSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage)) subscriptions, err := NewSubscriptionFinder(nil).findSubscriptionsFromWebPage("http://example.org/", "text/html", strings.NewReader(htmlPage))
if err != nil { if err != nil {
t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err) t.Fatalf(`Parsing a correctly formatted HTML page should not return any error: %v`, err)
} }

View file

@ -5,20 +5,20 @@ package subscription // import "miniflux.app/v2/internal/reader/subscription"
import "fmt" import "fmt"
// Subscription represents a feed subscription. // subscription represents a feed subscription.
type Subscription struct { type subscription struct {
Title string `json:"title"` Title string `json:"title"`
URL string `json:"url"` URL string `json:"url"`
Type string `json:"type"` Type string `json:"type"`
} }
func NewSubscription(title, url, kind string) *Subscription { func NewSubscription(title, url, kind string) *subscription {
return &Subscription{Title: title, URL: url, Type: kind} return &subscription{Title: title, URL: url, Type: kind}
} }
func (s Subscription) String() string { func (s subscription) String() string {
return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type) return fmt.Sprintf(`Title=%q, URL=%q, Type=%q`, s.Title, s.URL, s.Type)
} }
// Subscriptions represents a list of subscription. // Subscriptions represents a list of subscription.
type Subscriptions []*Subscription type Subscriptions []*subscription

View file

@ -10,14 +10,14 @@ import (
"miniflux.app/v2/internal/locale" "miniflux.app/v2/internal/locale"
) )
// AuthForm represents the authentication form. // authForm represents the authentication form.
type AuthForm struct { type authForm struct {
Username string Username string
Password string Password string
} }
// Validate makes sure the form values are valid. // Validate makes sure the form values are valid.
func (a AuthForm) Validate() *locale.LocalizedError { func (a authForm) Validate() *locale.LocalizedError {
if a.Username == "" || a.Password == "" { if a.Username == "" || a.Password == "" {
return locale.NewLocalizedError("error.fields_mandatory") return locale.NewLocalizedError("error.fields_mandatory")
} }
@ -26,8 +26,8 @@ func (a AuthForm) Validate() *locale.LocalizedError {
} }
// NewAuthForm returns a new AuthForm. // NewAuthForm returns a new AuthForm.
func NewAuthForm(r *http.Request) *AuthForm { func NewAuthForm(r *http.Request) *authForm {
return &AuthForm{ return &authForm{
Username: strings.TrimSpace(r.FormValue("username")), Username: strings.TrimSpace(r.FormValue("username")),
Password: strings.TrimSpace(r.FormValue("password")), Password: strings.TrimSpace(r.FormValue("password")),
} }

View file

@ -13,14 +13,14 @@ import (
"miniflux.app/v2/internal/validator" "miniflux.app/v2/internal/validator"
) )
// MarkReadBehavior list all possible behaviors for automatically marking an entry as read // markReadBehavior list all possible behaviors for automatically marking an entry as read
type MarkReadBehavior string type markReadBehavior string
const ( const (
NoAutoMarkAsRead MarkReadBehavior = "no-auto" NoAutoMarkAsRead markReadBehavior = "no-auto"
MarkAsReadOnView MarkReadBehavior = "on-view" MarkAsReadOnView markReadBehavior = "on-view"
MarkAsReadOnViewButWaitForPlayerCompletion MarkReadBehavior = "on-view-but-wait-for-player-completion" MarkAsReadOnViewButWaitForPlayerCompletion markReadBehavior = "on-view-but-wait-for-player-completion"
MarkAsReadOnlyOnPlayerCompletion MarkReadBehavior = "on-player-completion" MarkAsReadOnlyOnPlayerCompletion markReadBehavior = "on-player-completion"
) )
// SettingsForm represents the settings form. // SettingsForm represents the settings form.
@ -48,7 +48,7 @@ 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
@ -58,7 +58,7 @@ type SettingsForm struct {
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values. // MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
// Useful to convert the values from the User model to the form // Useful to convert the values from the User model to the form
func MarkAsReadBehavior(markReadOnView, markReadOnMediaPlayerCompletion bool) MarkReadBehavior { func MarkAsReadBehavior(markReadOnView, markReadOnMediaPlayerCompletion bool) markReadBehavior {
switch { switch {
case markReadOnView && !markReadOnMediaPlayerCompletion: case markReadOnView && !markReadOnMediaPlayerCompletion:
return MarkAsReadOnView return MarkAsReadOnView
@ -73,9 +73,9 @@ func MarkAsReadBehavior(markReadOnView, markReadOnMediaPlayerCompletion bool) Ma
} }
} }
// ExtractMarkAsReadBehavior returns the MarkReadOnView and MarkReadOnMediaPlayerCompletion values from the given MarkReadBehavior. // extractMarkAsReadBehavior returns the MarkReadOnView and MarkReadOnMediaPlayerCompletion values from the given MarkReadBehavior.
// Useful to extract the values from the form to the User model // Useful to extract the values from the form to the User model
func ExtractMarkAsReadBehavior(behavior MarkReadBehavior) (markReadOnView, markReadOnMediaPlayerCompletion bool) { func extractMarkAsReadBehavior(behavior markReadBehavior) (markReadOnView, markReadOnMediaPlayerCompletion bool) {
switch behavior { switch behavior {
case MarkAsReadOnView: case MarkAsReadOnView:
return true, false return true, false
@ -119,7 +119,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks user.AlwaysOpenExternalLinks = s.AlwaysOpenExternalLinks
user.OpenExternalLinksInNewTab = s.OpenExternalLinksInNewTab user.OpenExternalLinksInNewTab = s.OpenExternalLinksInNewTab
MarkReadOnView, MarkReadOnMediaPlayerCompletion := ExtractMarkAsReadBehavior(s.MarkReadBehavior) MarkReadOnView, MarkReadOnMediaPlayerCompletion := extractMarkAsReadBehavior(s.MarkReadBehavior)
user.MarkReadOnView = MarkReadOnView user.MarkReadOnView = MarkReadOnView
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
@ -205,7 +205,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
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"),

View file

@ -13,28 +13,28 @@ import (
"miniflux.app/v2/internal/ui/static" "miniflux.app/v2/internal/ui/static"
) )
// View wraps template argument building. // view wraps template argument building.
type View struct { type view struct {
tpl *template.Engine tpl *template.Engine
r *http.Request r *http.Request
params map[string]any params map[string]any
} }
// Set adds a new template argument. // Set adds a new template argument.
func (v *View) Set(param string, value any) *View { func (v *view) Set(param string, value any) *view {
v.params[param] = value v.params[param] = value
return v return v
} }
// Render executes the template with arguments. // Render executes the template with arguments.
func (v *View) Render(template string) []byte { func (v *view) Render(template string) []byte {
return v.tpl.Render(template+".html", v.params) return v.tpl.Render(template+".html", v.params)
} }
// New returns a new view with default parameters. // New returns a new view with default parameters.
func New(tpl *template.Engine, r *http.Request, sess *session.Session) *View { func New(tpl *template.Engine, r *http.Request, sess *session.Session) *view {
theme := request.UserTheme(r) theme := request.UserTheme(r)
return &View{tpl, r, map[string]any{ return &view{tpl, r, map[string]any{
"menu": "", "menu": "",
"csrf": request.CSRF(r), "csrf": request.CSRF(r),
"flashMessage": sess.FlashMessage(request.FlashMessage(r)), "flashMessage": sess.FlashMessage(request.FlashMessage(r)),

View file

@ -27,7 +27,7 @@ func NewPool(store *storage.Storage, nbWorkers int) *Pool {
} }
for i := range nbWorkers { for i := range nbWorkers {
worker := &Worker{id: i, store: store} worker := &worker{id: i, store: store}
go worker.Run(workerPool.queue) go worker.Run(workerPool.queue)
} }

View file

@ -14,14 +14,14 @@ import (
"miniflux.app/v2/internal/storage" "miniflux.app/v2/internal/storage"
) )
// Worker refreshes a feed in the background. // worker refreshes a feed in the background.
type Worker struct { type worker struct {
id int id int
store *storage.Storage store *storage.Storage
} }
// Run wait for a job and refresh the given feed. // Run wait for a job and refresh the given feed.
func (w *Worker) Run(c <-chan model.Job) { func (w *worker) Run(c <-chan model.Job) {
slog.Debug("Worker started", slog.Debug("Worker started",
slog.Int("worker_id", w.id), slog.Int("worker_id", w.id),
) )