8000 [pull] main from miniflux:main by pull[bot] · Pull Request #216 · suhaoGe/miniflux · GitHub
[go: up one dir, main page]

Skip to content
Merged 8000
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/reader/json/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
// The entry title is optional, so we need to find a fallback.
if entry.Title == "" {
for _, value := range []string{item.Summary, item.ContentText, item.ContentHTML} {
value = strings.TrimSpace(value)
if value != "" {
entry.Title = sanitizer.TruncateHTML(value, 100)
break
}
}
}
Expand Down Expand Up @@ -167,7 +169,7 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
entry.Tags = slices.Compact(entry.Tags)

// Generate a hash for the entry.
for _, value := range []string{item.ID, item.URL, item.ContentText + item.ContentHTML + item.Summary} {
for _, value := range []string{item.ID, item.URL, item.ExternalURL, item.ContentText + item.ContentHTML + item.Summary} {
value = strings.TrimSpace(value)
if value != "" {
entry.Hash = crypto.SHA256(value)
Expand Down
27 changes: 25 additions & 2 deletions internal/reader/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package json // import "miniflux.app/v2/internal/reader/json"

import "encoding/json"

// JSON Feed specs:
// https://www.jsonfeed.org/version/1.1/
// https://www.jsonfeed.org/version/1/
Expand Down Expand Up @@ -31,7 +33,7 @@ type JSONFeed struct {
FaviconURL string `json:"favicon"`

// Authors specifies one or more feed authors. The author object has several members.
Authors []JSONAuthor `json:"authors"` // JSON Feed v1.1
Authors JSONAuthors `json:"authors"` // JSON Feed v1.1

// Author specifies the feed author. The author object has several members.
// JSON Feed v1 (deprecated)
Expand Down Expand Up @@ -62,6 +64,27 @@ type JSONAuthor struct {
AvatarURL string `json:"avatar"`
}

// JSONAuthors unmarshals either an array or a single author object.
// Some feeds incorrectly use an object for "authors"; we accept it to avoid failing the whole feed.
type JSONAuthors []JSONAuthor

func (a *JSONAuthors) UnmarshalJSON(data []byte) error {
var authors []JSONAuthor
if err := json.Unmarshal(data, &authors); err == nil {
*a = authors
return nil
}

var author JSONAuthor
if err := json.Unmarshal(data, &author); err == nil {
*a = []JSONAuthor{author}
return nil
}

// Ign 10BC8 ore invalid formats silently; the caller can still use other fields.
return nil
}

type JSONHub struct {
// Type defines the protocol used to talk with the hub: "rssCloud" or "WebSub".
Type string `json:"type"`
Expand Down Expand Up @@ -112,7 +135,7 @@ type JSONItem struct {
Language string `json:"language"`

// Authors is an array of JSONAuthor.
Authors []JSONAuthor `json:"authors"`
Authors JSONAuthors `json:"authors"`

// Author is a JSONAuthor.
// JSON Feed v1 (deprecated)
Expand Down
2 changes: 1 addition & 1 deletion internal/reader/json/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// Parse returns a normalized feed struct from a JSON feed.
func Parse(baseURL string, data io.Reader) (*model.Feed, error) {
jsonFeed := new(JSONFeed)
if err := json.NewDecoder(data).Decode(&jsonFeed); err != nil {
if err := json.NewDecoder(data).Decode(jsonFeed); err != nil {
return nil, fmt.Errorf("json: unable to parse feed: %w", err)
}

Expand Down
168 changes: 146 additions & 22 deletions internal/reader/json/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ package json // import "miniflux.app/v2/internal/reader/json"

import (
"bytes"
"strings"
"crypto/sha256"
"encoding/hex"
"testing"
"time"
)
Expand Down Expand Up @@ -598,6 +599,39 @@ func TestParseItemWithMultipleDuplicateAuthors(t *testing.T) {
}
}

func TestParseItemWithAuthorsObject(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1.1",
"title": "Example",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{< 10BC8 /td>
"id": "1",
"title": "Example Item",
"url": "https://example.org/item",
"date_published": "2020-01-02T03:04:05Z",
"authors": {
"name": "Example Author"
}
}
]
}`

feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}

if len(feed.Entries) != 1 {
t.Fatalf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if feed.Entries[0].Author != "Example Author" {
t.Errorf("Incorrect entry author, got: %s", feed.Entries[0].Author)
}
}

func TestParseItemWithInvalidDate(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
Expand Down Expand Up @@ -629,15 +663,17 @@ func TestParseItemWithInvalidDate(t *testing.T) {
}
}

func TestParseItemWithoutTitleButWithURL(t *testing.T) {
func TestParseItemWithMissingTitleUsesSummaryFallback(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"url": "https://example.org/item"
"summary": "Summary title",
"content_text": "Content text title",
"content_html": "<p>HTML title</p>"
}
]
}`
Expand All @@ -651,20 +687,22 @@ func TestParseItemWithoutTitleButWithURL(t *testing.T) {
t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if feed.Entries[0].Title != "https://example.org/item" {
if feed.Entries[0].Title != "Summary title" {
t.Errorf("Incorrect entry title, got: %s", feed.Entries[0].Title)
}
}

func TestParseItemWithoutTitleButWithSummary(t *testing.T) {
func TestParseItemWithMissingTitleUsesContentTextFallback(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"summary": "This is some text content."
"summary": " ",
"content_text": "Content text title",
"content_html": "<p>HTML title</p>"
}
]
}`
Expand All @@ -678,20 +716,22 @@ func TestParseItemWithoutTitleButWithSummary(t *testing.T) {
t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if feed.Entries[0].Title != "This is some text content." {
if feed.Entries[0].Title != "Content text title" {
t.Errorf("Incorrect entry title, got: %s", feed.Entries[0].Title)
}
}

func TestParseItemWithoutTitleButWithHTMLContent(t *testing.T) {
func TestParseItemWithMissingTitleUsesHTMLFallback(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"content_html": "This is <strong>HTML</strong>."
"summary": "",
"content_text": "",
"content_html": "<p>HTML title</p>"
}
]
}`
8 Expand All @@ -705,20 +745,20 @@ func TestParseItemWithoutTitleButWithHTMLContent(t *testing.T) {
t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if feed.Entries[0].Title != "This is HTML." {
if feed.Entries[0].Title != "HTML title" {
t.Errorf("Incorrect entry title, got: %s", feed.Entries[0].Title)
}
}

func TestParseItemWithoutTitleButWithTextContent(t *testing.T) {
func TestParseItemWithMissingTitleUsesURLFallback(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"content_text": "` + strings.Repeat("a", 200) + `"
"url": "https://example.org/item"
}
]
}`
Expand All @@ -732,11 +772,7 @@ func TestParseItemWithoutTitleButWithTextContent(t *testing.T) {
t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if len(feed.Entries[0].Title) != 103 {
t.Errorf("Incorrect entry title, got: %d", len(feed.Entries[0].Title))
}

if len([]rune(feed.Entries[0].Title)) != 101 {
if feed.Entries[0].Title != "https://example.org/item" {
t.Errorf("Incorrect entry title, got: %s", feed.Entries[0].Title)
}
}
Expand Down Expand Up @@ -799,15 +835,64 @@ func TestParseItemTitleWithXMLTags(t *testing.T) {
}
}

func TestParseItemWithoutID(t *testing.T) {
func TestParseItemHashPrefersIDOverURL(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "My Example Feed",
"title": "Example",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"id": "id-value",
"url": "https://example.org/article"
}
]
}`

feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}

expected := sha256.Sum256([]byte("id-value"))
if feed.Entries[0].Hash != hex.EncodeToString(expected[:]) {
t.Errorf("Incorrect entry hash, got: %s", feed.Entries[0].Hash)
}
}

func TestParseItemHashUsesURLWhenNoID(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "Example",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"url": "https://example.org/article"
}
]
}`

feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}

expected := sha256.Sum256([]byte("https://example.org/article"))
if feed.Entries[0].Hash != hex.EncodeToString(expected[:]) {
t.Errorf("Incorrect entry hash, got: %s", feed.Entries[0].Hash)
}
}

func TestParseItemHashUsesExternalURLFallback(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "Example",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"content_text": "Some text."
"external_url": "https://example.org/external"
}
]
}`
Expand All @@ -818,10 +903,37 @@ func TestParseItemWithoutID(t *testing.T) {
}

if len(feed.Entries) != 1 {
t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
t.Fatalf("Incorrect number of entries, got: %d", len(feed.Entries))
}

if feed.Entries[0].Hash != "13b4c5aecd1b6d749afcee968fbf9c80f1ed1bbdbe1aaf25cb34ebd01144bbe9" {
expected := sha256.Sum256([]byte("https://example.org/external"))
if feed.Entries[0].Hash != hex.EncodeToString(expected[:]) {
t.Errorf("Incorrect entry hash, got: %s", feed.Entries[0].Hash)
}
}

func TestParseItemHashFallsBackToContent(t *testing.T) {
data := `{
"version": "https://jsonfeed.org/version/1",
"title": "Example",
"home_page_url": "https://example.org/",
"feed_url": "https://example.org/feed.json",
"items": [
{
"content_text": "Text",
"content_html": "<p>HTML</p>",
"summary": "Summary"
}
]
}`

feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatal(err)
}

expected := sha256.Sum256([]byte("Text<p>HTML</p>Summary"))
if feed.Entries[0].Hash != hex.EncodeToString(expected[:]) {
t.Errorf("Incorrect entry hash, got: %s", feed.Entries[0].Hash)
}
}
Expand Down Expand Up @@ -979,3 +1091,15 @@ func TestParseInvalidJSON(t *testing.T) {
t.Error("Parse should returns an error")
}
}

func TestParseNullJSONFeed(t *testing.T) {
data := `null`
feed, err := Parse("https://example.org/feed.json", bytes.NewBufferString(data))
if err != nil {
t.Fatalf("Unexpected error when parsing null feed: %v", err)
}

if feed == nil {
t.Fatalf("Feed should not be nil")
}
}
Loading
0