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/
-
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 }]);