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:
parent
a4d51b5586
commit
566670cc06
36 changed files with 369 additions and 376 deletions
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7">valid</a></p>`
|
input := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7">valid</a></p>`
|
||||||
expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
expected := `<p>This link is <a href="magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := 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&body=My%20idea%20is%3A%20%0A">valid</a></p>`
|
input := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A">valid</a></p>`
|
||||||
expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
expected := `<p>This link is <a href="mailto:jsmith@example.com?subject=A%20Test&body=My%20idea%20is%3A%20%0A" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := 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&priority=urgent">valid</a></p>`
|
input = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent">valid</a></p>`
|
||||||
expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
expected = `<p>This link is <a href="sips:alice@atlanta.com?subject=project%20x&priority=urgent" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output = 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&type=subscribed">valid</a></p>`
|
input := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed">valid</a></p>`
|
||||||
expected := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
expected := `<p>This link is <a href="xmpp:user@host?subscribe&type=subscribed" rel="noopener noreferrer" referrerpolicy="no-referrer" target="_blank">valid</a></p>`
|
||||||
output := 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" > /etc/hosts</pre>`
|
input := `<pre>echo "test" > /etc/hosts</pre>`
|
||||||
expected := `<pre>echo "test" > /etc/hosts</pre>`
|
expected := `<pre>echo "test" > /etc/hosts</pre>`
|
||||||
output := 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="<b>test</b>">test</td>`
|
expected := `<td rowspan="<b>test</b>">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&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
input := `<iframe src="http://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||||
output := 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&controls=0"></iframe>`
|
input := `<iframe src="https://www.youtube.com/embed/test123?rel=0&controls=0"></iframe>`
|
||||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||||
output := 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&controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
|
input := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin"></iframe>`
|
||||||
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
expected := `<iframe src="https://www.youtube-nocookie.com/embed/test123?rel=0&controls=0" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||||
output := 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&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
input := `<iframe src="https://www.youtube.com/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent"></iframe>`
|
||||||
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
expected := `<iframe src="https://invidious.custom/embed/test123?version=3&rel=1&fs=1&autohide=2&showsearch=0&showinfo=1&iv_load_policy=1&wmode=transparent" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||||
output := 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&byline=0"></iframe>`
|
input := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0"></iframe>`
|
||||||
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0&dnt=1" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
expected := `<iframe src="https://player.vimeo.com/video/123456?title=0&byline=0&dnt=1" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
|
||||||
output := 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)
|
||||||
|
|
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue