Merge commit from fork

This commit is contained in:
mr. m
2026-05-03 22:39:44 +02:00
committed by GitHub
parent 3278a43751
commit 607551f394
6 changed files with 301 additions and 10 deletions

View File

@@ -0,0 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.live-folders.github.skip-new-pr-ui-check
value: false

View File

@@ -34,7 +34,11 @@ export class nsGithubLiveFolderProvider extends nsZenLiveFolderProvider {
if (
this.state.type === "pull-requests" &&
typeof this.state.isJsonApi !== "boolean"
typeof this.state.isJsonApi !== "boolean" &&
!Services.prefs.getBoolPref(
"zen.live-folders.github.skip-new-pr-ui-check",
false
)
) {
const { text, status } = await this.fetch(this.state.url, {
headers: {

View File

@@ -62,6 +62,14 @@ export class nsRssLiveFolderProvider extends nsZenLiveFolderProvider {
if (!item.url || !item.date) {
return false;
}
try {
const parsed = Services.io.newURI(item.url);
if (parsed.scheme !== "http" && parsed.scheme !== "https") {
return false;
}
} catch {
return false;
}
if (!this.state.timeRange) {
return true;
}
@@ -73,10 +81,9 @@ export class nsRssLiveFolderProvider extends nsZenLiveFolderProvider {
for (let item of items) {
if (item.url) {
try {
const url = new URL(item.url);
const favicon = await lazy.PlacesUtils.favicons.getFaviconForPage(
Services.io.newURI(url.href)
);
const url = Services.io.newURI(item.url);
const favicon =
await lazy.PlacesUtils.favicons.getFaviconForPage(url);
item.icon =
favicon?.dataURI.spec ||
this.manager.window.gZenEmojiPicker.getSVGURL("logo-rss.svg");

View File

@@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
prefs = ["zen.live-folders.github.skip-new-pr-ui-check=true"]
["browser_github_live_folder.js"]

View File

@@ -65,10 +65,10 @@ add_task(async function test_fetch_items_url_construction() {
const fetchedUrl = new URL(instance.fetch.firstCall.args[0]);
const searchParams = fetchedUrl.searchParams;
Assert.ok(fetchedUrl.href.startsWith("https://github.com/issues/assigned"));
Assert.ok(fetchedUrl.href.startsWith("https://github.com/pulls"));
const query = searchParams.get("q");
Assert.ok(query.includes("state:open"), "Should include state:open");
Assert.ok(query.includes("is:open"), "Should include state:open");
Assert.ok(query.includes("is:pr"), "Should include is:PR");
Assert.ok(query.includes("author:@me"), "Should include author:@me");
Assert.ok(!query.includes("assignee:@me"), "Should NOT include assignee:@me");
@@ -176,3 +176,169 @@ add_task(async function test_fetch_network_error() {
sandbox.restore();
});
add_task(async function test_no_filter_enabled_returns_error() {
info(
"should short-circuit and return the no-filter error when every option is off"
);
let sandbox = sinon.createSandbox();
let instance = getGithubProviderForTest(sandbox, {
type: "pull-requests",
authorMe: false,
assignedMe: false,
reviewRequested: false,
});
const result = await instance.fetchItems();
Assert.equal(
result,
"zen-live-folder-github-no-filter",
"Should return the no-filter error id"
);
Assert.ok(
instance.fetch.notCalled,
"Should not issue a fetch when no filter is enabled"
);
sandbox.restore();
});
add_task(async function test_404_returns_no_auth() {
info("should treat a 404 as a missing-auth signal");
let sandbox = sinon.createSandbox();
let instance = getGithubProviderForTest(sandbox, {
type: "pull-requests",
authorMe: true,
});
instance.fetch.resolves({ status: 404, text: "" });
const result = await instance.fetchItems();
Assert.equal(
result,
"zen-live-folder-github-no-auth",
"Should return the no-auth error id"
);
sandbox.restore();
});
add_task(async function test_repo_excludes_emit_negative_repo_filters() {
info("should add -repo:<repo> clauses for each excluded repository");
let sandbox = sinon.createSandbox();
let instance = getGithubProviderForTest(sandbox, {
type: "pull-requests",
authorMe: true,
assignedMe: false,
reviewRequested: false,
repoExcludes: ["zen-browser/desktop", "foo/bar"],
});
instance.fetch.resolves({ status: 200, text: "<html></html>" });
await instance.fetchItems();
const fetchedUrl = new URL(instance.fetch.firstCall.args[0]);
const query = fetchedUrl.searchParams.get("q");
Assert.ok(
query.includes("-repo:zen-browser/desktop"),
"Should exclude zen-browser/desktop from the query"
);
Assert.ok(
query.includes("-repo:foo/bar"),
"Should exclude foo/bar from the query"
);
sandbox.restore();
});
add_task(async function test_pull_requests_json_api_parsing() {
info("should parse the new PR dashboard JSON payload");
let sandbox = sinon.createSandbox();
let instance = getGithubProviderForTest(sandbox, {
type: "pull-requests",
authorMe: true,
});
const payload = JSON.stringify({
payload: {
pullsDashboardSurfaceContentRoute: {
results: [
{
repoNameWithOwner: "zen-browser/desktop",
number: 42,
title: "Add live folders",
author: { displayLogin: "alice" },
permalink: "https://github.com/zen-browser/desktop/pull/42",
},
{
repoNameWithOwner: "zen-browser/desktop",
number: 43,
title: "Fix bug",
author: { displayLogin: "bob" },
permalink: "https://github.com/zen-browser/desktop/pull/43",
},
],
},
},
});
instance.fetch.resolves({ status: 200, text: payload });
const items = await instance.fetchItems();
Assert.equal(items.length, 2, "Should parse two PRs from the JSON payload");
Assert.equal(items[0].id, "zen-browser/desktop#42");
Assert.equal(items[0].title, "Add live folders");
Assert.equal(items[0].subtitle, "alice");
Assert.equal(items[0].url, "https://github.com/zen-browser/desktop/pull/42");
Assert.equal(items[1].id, "zen-browser/desktop#43");
Assert.ok(
instance.state.isJsonApi,
"Should mark the provider as using the JSON API"
);
sandbox.restore();
});
add_task(async function test_pull_requests_json_api_falls_back_to_html() {
info(
"should fall back to HTML parsing when an HTML response arrives unexpectedly"
);
let sandbox = sinon.createSandbox();
let instance = getGithubProviderForTest(sandbox, {
type: "pull-requests",
authorMe: true,
});
// Simulate a previous fetch having locked the provider into JSON-API mode.
instance.state.isJsonApi = true;
instance.fetch.resolves({
status: 200,
text: "<html><body>not JSON</body></html>",
});
const result = await instance.fetchItems();
Assert.equal(
instance.state.isJsonApi,
false,
"Should clear isJsonApi after seeing a non-JSON response"
);
Assert.equal(
result,
"zen-live-folder-failed-fetch",
"Should surface a fetch error so the user is prompted to retry"
);
sandbox.restore();
});

View File

@@ -162,9 +162,9 @@ add_task(async function test_max_items_limit() {
const rssXml = `
<rss version="2.0">
<channel>
<item><title>1</title><link>1</link><pubDate>${date}</pubDate></item>
<item><title>2</title><link>2</link><pubDate>${date}</pubDate></item>
<item><title>3</title><link>3</link><pubDate>${date}</pubDate></item>
<item><title>1</title><link>https://example.com/1</link><pubDate>${date}</pubDate></item>
<item><title>2</title><link>https://example.com/2</link><pubDate>${date}</pubDate></item>
<item><title>3</title><link>https://example.com/3</link><pubDate>${date}</pubDate></item>
</channel>
</rss>
`;
@@ -218,6 +218,113 @@ add_task(async function test_invalid_dates() {
sandbox.restore();
});
add_task(async function test_item_url_scheme_filtering() {
info("should drop items whose link uses a non-http(s) scheme");
let sandbox = sinon.createSandbox();
let instance = getRssProviderForTest(sandbox, { timeRange: 0 });
const date = new Date().toUTCString();
const rssXml = `
<rss version="2.0">
<channel>
<item>
<title>JavaScript scheme</title>
<link>javascript:alert(1)</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>Data scheme</title>
<link>data:text/html,&lt;script&gt;alert(1)&lt;/script&gt;</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>File scheme</title>
<link>file:///etc/passwd</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>about: scheme</title>
<link>about:config</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>chrome: scheme</title>
<link>chrome://browser/content/browser.xhtml</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>Invalid URL</title>
<link>not a url</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>Good https</title>
<link>https://example.com/good</link>
<pubDate>${date}</pubDate>
</item>
<item>
<title>Good http</title>
<link>http://example.com/good</link>
<pubDate>${date}</pubDate>
</item>
</channel>
</rss>
`;
instance.fetch.resolves({ text: rssXml });
const items = await instance.fetchItems();
Assert.equal(
items.length,
2,
"Only http(s) items should survive scheme filtering"
);
Assert.deepEqual(
items.map(i => i.url).sort(),
["http://example.com/good", "https://example.com/good"],
"Surviving items should be the http and https links"
);
sandbox.restore();
});
add_task(async function test_atom_item_url_scheme_filtering() {
info("should drop Atom entries whose link href uses a non-http(s) scheme");
let sandbox = sinon.createSandbox();
let instance = getRssProviderForTest(sandbox, { timeRange: 0 });
const updated = new Date().toISOString();
const atomXml = `
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Atom Feed</title>
<entry>
<title>Bad scheme</title>
<link href="javascript:alert(1)" />
<id>urn:uuid:bad</id>
<updated>${updated}</updated>
</entry>
<entry>
<title>Good scheme</title>
<link href="https://example.com/atom-good" />
<id>urn:uuid:good</id>
<updated>${updated}</updated>
</entry>
</feed>
`;
instance.fetch.resolves({ text: atomXml });
const items = await instance.fetchItems();
Assert.equal(items.length, 1, "Only the https Atom entry should remain");
Assert.equal(items[0].url, "https://example.com/atom-good");
sandbox.restore();
});
add_task(async function test_fetch_network_error() {
info("should return empty array on network error");