Skip to content
Draft
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
38 changes: 38 additions & 0 deletions src/parser/__test__/entity-expansion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,42 @@ describe("entity expansion limit", () => {
expect(result?.items).toHaveLength(1);
expect(result?.items?.[0]).toHaveProperty("title", "Item");
});

it("does not count deprecated itunes:summary entities when description exists", () => {
const entityRef = "<p>"hello"</p>";
const count = 100001;
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel>
<title>Entity expansion test</title>
<description>Channel description</description>
<itunes:summary>${entityRef.repeat(count)}</itunes:summary>
<item>
<title>Item</title>
<description>Item description</description>
<itunes:summary>${entityRef.repeat(count)}</itunes:summary>
<enclosure url="https://example.com/ep.mp3" length="1000" type="audio/mpeg"/>
</item>
</channel>
</rss>`;

const result = parseFeed(xml, { allowMissingGuid: true });

expect(result).not.toBeNull();
expect(result).toHaveProperty("description", "Channel description");
expect(result).not.toHaveProperty("summary");
expect(result?.items?.[0]).toHaveProperty("description", "Item description");
expect(result?.items?.[0]).not.toHaveProperty("summary");
});

it("still rejects too many entities outside deprecated itunes:summary", () => {
const entityRef = "&amp; ";
const count = 100001;
const descriptionContent = entityRef.repeat(count);
const xml = minimalRssPrefix + descriptionContent + minimalRssSuffix;

expect(() => parseFeed(xml, { allowMissingGuid: true })).toThrow(
"Entity expansion count limit exceeded"
);
});
});
29 changes: 28 additions & 1 deletion src/parser/__test__/feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ describe("feed handling", () => {
expect(result).toHaveProperty("description", "<p>bye</p>");
});

it("decodes itunes:summary only when it is used as a fallback", () => {
const xml = helpers.spliceFeed(
feed,
`
<itunes:summary>&lt;p&gt;bye&lt;/p&gt;</itunes:summary>
`
);

const result = parseFeed(xml);

expect(result).toHaveProperty("description", "<p>bye</p>");
expect(result).not.toHaveProperty("summary");
});

it("prefers description value when the fall back exists", () => {
const xml = helpers.spliceFeed(
feed,
Expand Down Expand Up @@ -173,7 +187,7 @@ describe("feed handling", () => {
});

describe("summary", () => {
it("extracts the value", () => {
it("ignores the deprecated value by default", () => {
const xml = helpers.spliceFeed(
feed,
`
Expand All @@ -183,6 +197,19 @@ describe("feed handling", () => {

const result = parseFeed(xml);

expect(result).not.toHaveProperty("summary");
});

it("extracts the value when requested", () => {
const xml = helpers.spliceFeed(
feed,
`
<itunes:summary>hi</itunes:summary>
`
);

const result = parseFeed(xml, { includeItunesSummary: true });

expect(result).toHaveProperty("summary", "hi");
});
});
Expand Down
42 changes: 40 additions & 2 deletions src/parser/__test__/item.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,25 @@ describe("item handling", () => {
expect(first).toHaveProperty("description", "<p>bye</p>");
});

it("decodes itunes:summary only when it is used as a fallback", () => {
const xml = helpers.spliceFeed(
feed,
`
<item>
<itunes:summary>&lt;p&gt;bye&lt;/p&gt;</itunes:summary>
<guid isPermaLink="true">https://example.com/ep0003</guid>
<enclosure url="https://aphid.fireside.fm/d/1437767933/65632ad5-59b2-4e30-82d1-13845dce07dd/d11384ea-69b5-4e33-bd0e-5d33fdba8a0d.mp3" length="78034115" type="audio/mpeg"/>
</item>
`
);

const result = parseFeed(xml);
const [first] = result.items;

expect(first).toHaveProperty("description", "<p>bye</p>");
expect(first).not.toHaveProperty("summary");
});

it("prefers content:encoded value when falling back", () => {
const xml = helpers.spliceFeed(
feed,
Expand All @@ -1441,7 +1460,7 @@ describe("item handling", () => {
});

describe("summary", () => {
it("extracts the value", () => {
it("ignores the deprecated value by default", () => {
const xml = helpers.spliceFeed(
feed,
`
Expand All @@ -1457,6 +1476,25 @@ describe("item handling", () => {
const result = parseFeed(xml);
const [first] = result.items;

expect(first).not.toHaveProperty("summary");
});

it("extracts the value when requested", () => {
const xml = helpers.spliceFeed(
feed,
`
<item>
<itunes:summary>
<![CDATA[ <p>bye</p> ]]>
</itunes:summary> <guid isPermaLink="true">https://example.com/ep0003</guid>
<enclosure url="https://aphid.fireside.fm/d/1437767933/65632ad5-59b2-4e30-82d1-13845dce07dd/d11384ea-69b5-4e33-bd0e-5d33fdba8a0d.mp3" length="78034115" type="audio/mpeg"/>
</item>
`
);

const result = parseFeed(xml, { includeItunesSummary: true });
const [first] = result.items;

expect(first).toHaveProperty("summary", "<p>bye</p>");
});

Expand All @@ -1473,7 +1511,7 @@ describe("item handling", () => {
`
);

const result = parseFeed(xml);
const result = parseFeed(xml, { includeItunesSummary: true });
const [first] = result.items;

expect(first).toHaveProperty("summary", "bye");
Expand Down
9 changes: 6 additions & 3 deletions src/parser/feed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import he from "he";

import { logger } from "../logger";

import {
Expand All @@ -18,6 +20,7 @@ import {
import { BasicFeed, ItunesFeedType } from "./types";
import type { FeedType, XmlNode } from "./types";
import { categoryLookup } from "./itunes-categories";
import type { ParserOptions } from "./unified";

function getTitle(feed: XmlNode): string {
const node = firstWithValue(feed.title);
Expand Down Expand Up @@ -85,7 +88,7 @@ function getSummary(feed: XmlNode): undefined | { summary: string } {
if (node) {
const nodeValue = getText(node);
if (nodeValue) {
return { summary: sanitizeMultipleSpaces(sanitizeNewLines(nodeValue)) };
return { summary: sanitizeMultipleSpaces(sanitizeNewLines(he.decode(nodeValue))) };
}
}
return undefined;
Expand Down Expand Up @@ -627,7 +630,7 @@ function getTimeToLive(feed: XmlNode): undefined | { ttl: number } {
return undefined;
}

export function handleFeed(feed: XmlNode, feedType: FeedType): BasicFeed {
export function handleFeed(feed: XmlNode, feedType: FeedType, options?: ParserOptions): BasicFeed {
return {
lastUpdate: new Date(),
type: feedType,
Expand All @@ -650,7 +653,7 @@ export function handleFeed(feed: XmlNode, feedType: FeedType): BasicFeed {
...getAuthor(feed),
...getOwner(feed),
...getImage(feed),
...getSummary(feed),
...(options?.includeItunesSummary ? getSummary(feed) : undefined),
...getSubtitle(feed),
...getItunesTitle(feed),
...getCopyright(feed),
Expand Down
9 changes: 6 additions & 3 deletions src/parser/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */

import he from "he";

import { logger } from "../logger";

import {
Expand All @@ -23,6 +25,7 @@ import {
} from "./shared";
import type { BasicFeed, Enclosure, Episode, XmlNode } from "./types";
import { unescape } from "./unescape";
import type { ParserOptions } from "./unified";

export enum ItunesEpisodeType {
Full = "full",
Expand Down Expand Up @@ -292,7 +295,7 @@ function getSummary(item: XmlNode): undefined | { summary: string } {
if (node) {
const summaryValue = getText(node);
if (summaryValue) {
return { summary: summaryValue };
return { summary: he.decode(summaryValue) };
}
}

Expand All @@ -311,7 +314,7 @@ function getSubtitle(item: XmlNode): undefined | { subtitle: string } {
return undefined;
}

export function handleItem(item: XmlNode, _feed: BasicFeed): Episode {
export function handleItem(item: XmlNode, _feed: BasicFeed, options?: ParserOptions): Episode {
return {
guid: getGuid(item),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand All @@ -329,7 +332,7 @@ export function handleItem(item: XmlNode, _feed: BasicFeed): Episode {
...getKeywords(item),
...getPubDate(item),
...getImage(item),
...getSummary(item),
...(options?.includeItunesSummary ? getSummary(item) : undefined),
...getDescription(item),
...getSubtitle(item),
...getContent(item),
Expand Down
6 changes: 4 additions & 2 deletions src/parser/unified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type ParserOptions = {
allowInsecureHTTPMetaboost?: boolean;
/** Max UTF-8 bytes for the XML string. When set, overrides PARSER_MAX_FEED_BODY_BYTES and the 10 MiB default. */
maxFeedBodyBytes?: number;
/** Include deprecated itunes:summary fields as summary properties. Description fallback still uses itunes:summary when needed. */
includeItunesSummary?: boolean;
};

function handlePodcastSeasons(feedObj: BasicFeed) {
Expand Down Expand Up @@ -63,7 +65,7 @@ export function unifiedParser(theFeed: XmlNode, type: FeedType, options?: Parser
return null;
}

let feedObj = handleFeed(theFeed.rss.channel, type);
let feedObj = handleFeed(theFeed.rss.channel, type, options);

let phaseSupport: PhaseUpdate = {};

Expand All @@ -84,7 +86,7 @@ export function unifiedParser(theFeed: XmlNode, type: FeedType, options?: Parser
return undefined;
}

let newFeedItem: Episode = handleItem(item, feedObj);
let newFeedItem: Episode = handleItem(item, feedObj, options);

// Item Phase Support
const itemResult = updateItem(item, theFeed, undefined, options);
Expand Down
8 changes: 7 additions & 1 deletion src/parser/xml-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ const parserOptions = {
parseAttributeValue: false,
trimValues: true,
parseTrueNumberOnly: false,
tagValueProcessor: (_tagName: string, tagValue: string) => he.decode(tagValue),
tagValueProcessor: (_tagName: string, tagValue: string) => {
if (_tagName === "itunes:summary") {
return tagValue;
}
return he.decode(tagValue);
},
attributeValueProcessor: (_tagName: string, tagValue: string) => he.decode(tagValue),
stopNodes: ["parse-me-as-string"],
processEntities: {
enabled: true,
maxEntityCount: ENTITY_EXPANSION_LIMIT,
maxTotalExpansions: ENTITY_EXPANSION_LIMIT,
tagFilter: (tagName: string) => tagName !== "itunes:summary",
},
};

Expand Down