From 607551f39457efd734c09b3d7e0f9f292fb588bc Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sun, 3 May 2026 22:39:44 +0200 Subject: [PATCH] Merge commit from fork --- prefs/zen/live-folders.yaml | 6 + .../providers/GithubLiveFolder.sys.mjs | 6 +- .../providers/RssLiveFolder.sys.mjs | 15 +- src/zen/tests/live-folders/browser.toml | 1 + .../browser_github_live_folder.js | 170 +++++++++++++++++- .../live-folders/browser_rss_live_folder.js | 113 +++++++++++- 6 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 prefs/zen/live-folders.yaml diff --git a/prefs/zen/live-folders.yaml b/prefs/zen/live-folders.yaml new file mode 100644 index 000000000..f6573e637 --- /dev/null +++ b/prefs/zen/live-folders.yaml @@ -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 diff --git a/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs b/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs index 398ebaa77..b30520da0 100644 --- a/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs +++ b/src/zen/live-folders/providers/GithubLiveFolder.sys.mjs @@ -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: { diff --git a/src/zen/live-folders/providers/RssLiveFolder.sys.mjs b/src/zen/live-folders/providers/RssLiveFolder.sys.mjs index eff6bed3c..ab99fa4ad 100644 --- a/src/zen/live-folders/providers/RssLiveFolder.sys.mjs +++ b/src/zen/live-folders/providers/RssLiveFolder.sys.mjs @@ -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"); diff --git a/src/zen/tests/live-folders/browser.toml b/src/zen/tests/live-folders/browser.toml index bd75b3725..ccd76d2cb 100644 --- a/src/zen/tests/live-folders/browser.toml +++ b/src/zen/tests/live-folders/browser.toml @@ -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"] diff --git a/src/zen/tests/live-folders/browser_github_live_folder.js b/src/zen/tests/live-folders/browser_github_live_folder.js index ded5e5116..ee1237438 100644 --- a/src/zen/tests/live-folders/browser_github_live_folder.js +++ b/src/zen/tests/live-folders/browser_github_live_folder.js @@ -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: 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: "" }); + + 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: "not JSON", + }); + + 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(); +}); diff --git a/src/zen/tests/live-folders/browser_rss_live_folder.js b/src/zen/tests/live-folders/browser_rss_live_folder.js index aef39b9da..7b28a8f48 100644 --- a/src/zen/tests/live-folders/browser_rss_live_folder.js +++ b/src/zen/tests/live-folders/browser_rss_live_folder.js @@ -162,9 +162,9 @@ add_task(async function test_max_items_limit() { const rssXml = ` - 11${date} - 22${date} - 33${date} + 1https://example.com/1${date} + 2https://example.com/2${date} + 3https://example.com/3${date} `; @@ -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 = ` + + + + JavaScript scheme + javascript:alert(1) + ${date} + + + Data scheme + data:text/html,<script>alert(1)</script> + ${date} + + + File scheme + file:///etc/passwd + ${date} + + + about: scheme + about:config + ${date} + + + chrome: scheme + chrome://browser/content/browser.xhtml + ${date} + + + Invalid URL + not a url + ${date} + + + Good https + https://example.com/good + ${date} + + + Good http + http://example.com/good + ${date} + + + + `; + + 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 = ` + + Atom Feed + + Bad scheme + + urn:uuid:bad + ${updated} + + + Good scheme + + urn:uuid:good + ${updated} + + + `; + + 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");