From 25593713329414a3800044d506ad8aeef9394bb7 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:15:13 -0700 Subject: [PATCH] dont do the baseurl thing at all --- src/widgets/feed/proxy.js | 2 +- src/widgets/feed/proxy.test.js | 19 +++++++++++++++++++ src/widgets/feed/utils.js | 21 +++++++++++++++------ src/widgets/feed/utils.test.js | 20 +++++++++++++++++--- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/widgets/feed/proxy.js b/src/widgets/feed/proxy.js index 56f716874..3d2a95d7c 100644 --- a/src/widgets/feed/proxy.js +++ b/src/widgets/feed/proxy.js @@ -41,7 +41,7 @@ export default async function feedProxyHandler(req, res) { } try { - items = parseFeed(Buffer.from(data).toString(), url.href); + items = parseFeed(Buffer.from(data).toString()); } catch (e) { logger.debug("Error parsing feed %s//%s%s: %s", url.protocol, url.host, url.pathname, e.message); return res.status(500).json({ error: { message: "Invalid feed", url: sanitizeErrorURL(url) } }); diff --git a/src/widgets/feed/proxy.test.js b/src/widgets/feed/proxy.test.js index 30b4e9467..a8d7f1f2b 100644 --- a/src/widgets/feed/proxy.test.js +++ b/src/widgets/feed/proxy.test.js @@ -118,6 +118,25 @@ describe("widgets/feed/proxy", () => { expect(res.body).toEqual({ error: { message: "HTTP Error", url: "example.com (see logs for details)" } }); }); + it("never includes any part of the configured url in the response", async () => { + getServiceWidget.mockResolvedValue({ + type: "feed", + url: "https://user:hunter2@nas.lan/feeds/s3cr3t/rss.xml?token=abc123", + }); + const relative = ` + Pathitem/1 + Root/x + Fragment#top + `; + httpProxy.mockResolvedValueOnce([200, "application/rss+xml", Buffer.from(relative)]); + + const res = createMockRes(); + await feedProxyHandler(req, res); + + expect(res.body.items.map((item) => item.link)).toEqual([null, null, null]); + expect(JSON.stringify(res.body)).not.toMatch(/user|hunter2|nas\.lan|s3cr3t|token|abc123/); + }); + it("returns 500 for unparseable feeds", async () => { getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" }); httpProxy.mockResolvedValueOnce([200, "text/html", Buffer.from("")]); diff --git a/src/widgets/feed/utils.js b/src/widgets/feed/utils.js index c43879220..3ef8f5371 100644 --- a/src/widgets/feed/utils.js +++ b/src/widgets/feed/utils.js @@ -74,9 +74,10 @@ function parseRssItem(item) { }; } +const alternateLink = (node) => asArray(node.link).find((l) => (attrs(l).rel ?? "alternate") === "alternate"); + function parseAtomEntry(entry) { - const links = asArray(entry.link); - const link = links.find((l) => (attrs(l).rel ?? "alternate") === "alternate") ?? links[0]; + const link = alternateLink(entry) ?? asArray(entry.link)[0]; return { title: getText(entry.title), link: attrs(link).href, @@ -86,13 +87,21 @@ function parseAtomEntry(entry) { }; } -export function parseFeed(xml, baseUrl) { +export function parseFeed(xml) { const doc = xml2js(xml, { compact: true }); let items; - if (doc.rss) items = asArray(doc.rss.channel?.item).map(parseRssItem); - else if (doc.feed) items = asArray(doc.feed.entry).map(parseAtomEntry); - else throw new Error("Unsupported feed format"); + let site; + if (doc.rss) { + items = asArray(doc.rss.channel?.item).map(parseRssItem); + site = getText(doc.rss.channel?.link); + } else if (doc.feed) { + items = asArray(doc.feed.entry).map(parseAtomEntry); + site = attrs(alternateLink(doc.feed)).href; + } else throw new Error("Unsupported feed format"); + + // resolve against the feed's own site link, never the configured url + const baseUrl = httpUrl(site) ?? undefined; return items .map((item) => ({ diff --git a/src/widgets/feed/utils.test.js b/src/widgets/feed/utils.test.js index 9694898ce..51e8bdcb2 100644 --- a/src/widgets/feed/utils.test.js +++ b/src/widgets/feed/utils.test.js @@ -6,6 +6,7 @@ const rss = ` Example + https://example.com/ <![CDATA[Tom & Jerry]]> https://example.com/one @@ -54,7 +55,7 @@ const atom = ` describe("widgets/feed/utils", () => { it("parses rss 2.0 items", () => { - expect(parseFeed(rss, "https://example.com/feed.xml")).toEqual([ + expect(parseFeed(rss)).toEqual([ { title: "Tom & Jerry", link: "https://example.com/one", @@ -72,7 +73,7 @@ describe("widgets/feed/utils", () => { }); it("parses atom entries", () => { - expect(parseFeed(atom, "https://example.com/atom.xml")).toEqual([ + expect(parseFeed(atom)).toEqual([ { title: "Fish & chips", link: "https://example.com/one", @@ -90,6 +91,7 @@ describe("widgets/feed/utils", () => { it("falls back to the first usable image in item html", () => { const xml = ` + https://example.com/ Content image Hi @@ -110,7 +112,7 @@ describe("widgets/feed/utils", () => { `; - expect(parseFeed(xml, "https://example.com/feed").map((item) => item.image)).toEqual([ + expect(parseFeed(xml).map((item) => item.image)).toEqual([ "https://example.com/big.jpg?w=925&h=925", "https://example.com/desc.png", "https://example.com/thumb.jpg", @@ -130,6 +132,18 @@ describe("widgets/feed/utils", () => { ]); }); + it("resolves relative urls against the feed's alternate link, never its self link", () => { + const entry = `Relative`; + const selfOnly = ` + ${entry}`; + const withSite = ` + + ${entry}`; + + expect(parseFeed(selfOnly)[0].link).toBeNull(); + expect(parseFeed(withSite)[0].link).toBe("https://example.com/posts/1"); + }); + it("handles single-item and empty feeds", () => { const single = "Only"; expect(parseFeed(single)).toEqual([{ title: "Only", link: null, date: null, image: null }]);