mirror of
https://github.com/gethomepage/homepage.git
synced 2026-09-30 07:51:17 -07:00
dont do the baseurl thing at all
Docker CI / Docker Build & Push (push) Canceled after 0s
Lint / Linting Checks (push) Canceled after 0s
Tests / vitest (1) (push) Canceled after 0s
Tests / vitest (2) (push) Canceled after 0s
Tests / vitest (3) (push) Canceled after 0s
Tests / vitest (4) (push) Canceled after 0s
Docker CI / Docker Build & Push (push) Canceled after 0s
Lint / Linting Checks (push) Canceled after 0s
Tests / vitest (1) (push) Canceled after 0s
Tests / vitest (2) (push) Canceled after 0s
Tests / vitest (3) (push) Canceled after 0s
Tests / vitest (4) (push) Canceled after 0s
This commit is contained in:
@@ -41,7 +41,7 @@ export default async function feedProxyHandler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
items = parseFeed(Buffer.from(data).toString(), url.href);
|
items = parseFeed(Buffer.from(data).toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug("Error parsing feed %s//%s%s: %s", url.protocol, url.host, url.pathname, e.message);
|
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) } });
|
return res.status(500).json({ error: { message: "Invalid feed", url: sanitizeErrorURL(url) } });
|
||||||
|
|||||||
@@ -118,6 +118,25 @@ describe("widgets/feed/proxy", () => {
|
|||||||
expect(res.body).toEqual({ error: { message: "HTTP Error", url: "example.com (see logs for details)" } });
|
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 = `<rss><channel>
|
||||||
|
<item><title>Path</title><link>item/1</link><enclosure url="img.jpg" type="image/jpeg" /></item>
|
||||||
|
<item><title>Root</title><link>/x</link></item>
|
||||||
|
<item><title>Fragment</title><link>#top</link></item>
|
||||||
|
</channel></rss>`;
|
||||||
|
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 () => {
|
it("returns 500 for unparseable feeds", async () => {
|
||||||
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" });
|
getServiceWidget.mockResolvedValue({ type: "feed", url: "https://example.com/feed.xml" });
|
||||||
httpProxy.mockResolvedValueOnce([200, "text/html", Buffer.from("<html></html>")]);
|
httpProxy.mockResolvedValueOnce([200, "text/html", Buffer.from("<html></html>")]);
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ function parseRssItem(item) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const alternateLink = (node) => asArray(node.link).find((l) => (attrs(l).rel ?? "alternate") === "alternate");
|
||||||
|
|
||||||
function parseAtomEntry(entry) {
|
function parseAtomEntry(entry) {
|
||||||
const links = asArray(entry.link);
|
const link = alternateLink(entry) ?? asArray(entry.link)[0];
|
||||||
const link = links.find((l) => (attrs(l).rel ?? "alternate") === "alternate") ?? links[0];
|
|
||||||
return {
|
return {
|
||||||
title: getText(entry.title),
|
title: getText(entry.title),
|
||||||
link: attrs(link).href,
|
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 });
|
const doc = xml2js(xml, { compact: true });
|
||||||
|
|
||||||
let items;
|
let items;
|
||||||
if (doc.rss) items = asArray(doc.rss.channel?.item).map(parseRssItem);
|
let site;
|
||||||
else if (doc.feed) items = asArray(doc.feed.entry).map(parseAtomEntry);
|
if (doc.rss) {
|
||||||
else throw new Error("Unsupported feed format");
|
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
|
return items
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const rss = `<?xml version="1.0"?>
|
|||||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Example</title>
|
<title>Example</title>
|
||||||
|
<link>https://example.com/</link>
|
||||||
<item>
|
<item>
|
||||||
<title><![CDATA[Tom & Jerry]]></title>
|
<title><![CDATA[Tom & Jerry]]></title>
|
||||||
<link>https://example.com/one</link>
|
<link>https://example.com/one</link>
|
||||||
@@ -54,7 +55,7 @@ const atom = `<?xml version="1.0" encoding="utf-8"?>
|
|||||||
|
|
||||||
describe("widgets/feed/utils", () => {
|
describe("widgets/feed/utils", () => {
|
||||||
it("parses rss 2.0 items", () => {
|
it("parses rss 2.0 items", () => {
|
||||||
expect(parseFeed(rss, "https://example.com/feed.xml")).toEqual([
|
expect(parseFeed(rss)).toEqual([
|
||||||
{
|
{
|
||||||
title: "Tom & Jerry",
|
title: "Tom & Jerry",
|
||||||
link: "https://example.com/one",
|
link: "https://example.com/one",
|
||||||
@@ -72,7 +73,7 @@ describe("widgets/feed/utils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("parses atom entries", () => {
|
it("parses atom entries", () => {
|
||||||
expect(parseFeed(atom, "https://example.com/atom.xml")).toEqual([
|
expect(parseFeed(atom)).toEqual([
|
||||||
{
|
{
|
||||||
title: "Fish & chips",
|
title: "Fish & chips",
|
||||||
link: "https://example.com/one",
|
link: "https://example.com/one",
|
||||||
@@ -90,6 +91,7 @@ describe("widgets/feed/utils", () => {
|
|||||||
|
|
||||||
it("falls back to the first usable image in item html", () => {
|
it("falls back to the first usable image in item html", () => {
|
||||||
const xml = `<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel>
|
const xml = `<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel>
|
||||||
|
<link>https://example.com/</link>
|
||||||
<item>
|
<item>
|
||||||
<title>Content image</title>
|
<title>Content image</title>
|
||||||
<content:encoded><![CDATA[<p>Hi <img src="https://t.example.com/p.gif" width="1" height="1">
|
<content:encoded><![CDATA[<p>Hi <img src="https://t.example.com/p.gif" width="1" height="1">
|
||||||
@@ -110,7 +112,7 @@ describe("widgets/feed/utils", () => {
|
|||||||
</item>
|
</item>
|
||||||
</channel></rss>`;
|
</channel></rss>`;
|
||||||
|
|
||||||
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/big.jpg?w=925&h=925",
|
||||||
"https://example.com/desc.png",
|
"https://example.com/desc.png",
|
||||||
"https://example.com/thumb.jpg",
|
"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 = `<entry><title>Relative</title><link href="/posts/1" /></entry>`;
|
||||||
|
const selfOnly = `<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<link rel="self" href="https://example.com/user.private.atom?token=abc123" />${entry}</feed>`;
|
||||||
|
const withSite = `<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<link rel="self" href="https://example.com/user.private.atom?token=abc123" />
|
||||||
|
<link rel="alternate" href="https://example.com/blog/" />${entry}</feed>`;
|
||||||
|
|
||||||
|
expect(parseFeed(selfOnly)[0].link).toBeNull();
|
||||||
|
expect(parseFeed(withSite)[0].link).toBe("https://example.com/posts/1");
|
||||||
|
});
|
||||||
|
|
||||||
it("handles single-item and empty feeds", () => {
|
it("handles single-item and empty feeds", () => {
|
||||||
const single = "<rss><channel><item><title>Only</title></item></channel></rss>";
|
const single = "<rss><channel><item><title>Only</title></item></channel></rss>";
|
||||||
expect(parseFeed(single)).toEqual([{ title: "Only", link: null, date: null, image: null }]);
|
expect(parseFeed(single)).toEqual([{ title: "Only", link: null, date: null, image: null }]);
|
||||||
|
|||||||
Reference in New Issue
Block a user