Skip to content
Open
20 changes: 20 additions & 0 deletions locales/en-US/browser/browser/zen-live-folders.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,23 @@ zen-live-folder-github-option-repo-list-note =

zen-live-folders-promotion-title = Live Folder Created!
zen-live-folders-promotion-description = Latest content from your RSS feeds or GitHub pull requests will appear here automatically.

zen-live-folder-github-prompt-instance = Enter the GitHub instance URL

zen-live-folder-github-option-instance =
.label = Instance: { $host }

zen-live-folder-github-invalid-url-title = Invalid GitHub URL
zen-live-folder-github-invalid-url-description = The URL must be a valid HTTPS address for a GitHub instance.

zen-live-folder-github-prompt-token = Enter your GitHub Personal Access Token

zen-live-folder-github-option-set-token =
.label = Set Access Token…

zen-live-folder-github-option-remove-token =
.label = Remove Access Token

zen-live-folder-github-token-expired =
.label = Access token expired
.tooltiptext = Your access token has expired or been revoked. Click to set a new one.
6 changes: 5 additions & 1 deletion src/zen/live-folders/ZenLiveFolder.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class nsZenLiveFolderProvider {
this.manager.saveState();
}

fetch(url, { maxContentLength = 5 * 1024 * 1024 } = {}) {
fetch(url, { maxContentLength = 5 * 1024 * 1024, headers = {} } = {}) {
const uri = lazy.NetUtil.newURI(url);
// TODO: Support userContextId when fetching, it should be inherited from the folder's
// current space context ID.
Expand Down Expand Up @@ -155,6 +155,10 @@ export class nsZenLiveFolderProvider {
triggeringPrincipal: principal,
}).QueryInterface(Ci.nsIHttpChannel);

for (const [name, value] of Object.entries(headers)) {
channel.setRequestHeader(name, value, false);
}

let httpStatus = null;
let contentType = "";
let headerCharset = null;
Expand Down
43 changes: 42 additions & 1 deletion src/zen/live-folders/ZenLiveFoldersManager.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
FeatureCallout: "resource:///modules/asrouter/FeatureCallout.sys.mjs",
GithubTokenManager: "resource:///modules/zen/GithubAuth.sys.mjs",
});

ChromeUtils.defineLazyGetter(
Expand Down Expand Up @@ -208,6 +209,7 @@ class nsZenLiveFoldersManager {
}

let url;
let host;
let label;
let icon;

Expand All @@ -225,11 +227,25 @@ class nsZenLiveFoldersManager {
break;
}
case "github": {
// First GitHub folder defaults to github.com, subsequent ones show prompt
if (this.hasGitHubLiveFolder()) {
host = await ProviderClass.promptForHost(this.window);
if (!host) {
return -1;
}
} else {
host = "https://github.com";
}

const [message] = await lazy.l10n.formatMessages([
{ id: `zen-live-folder-github-${providerType}` },
]);

label = message.attributes[0].value;
const hostname = new URL(host).hostname;
label =
hostname === "github.com"
? message.attributes[0].value
: `${message.attributes[0].value} (${hostname})`;
icon = "chrome://browser/skin/zen-icons/selectable/logo-github.svg";
break;
}
Expand All @@ -250,6 +266,7 @@ class nsZenLiveFoldersManager {
const config = {
state: this.#applyDefaultStateValues({
url,
host,
type: providerType,
}),
};
Expand Down Expand Up @@ -356,6 +373,21 @@ class nsZenLiveFoldersManager {
}

liveFolder.stop();

// Clean up stored PAT if this is a GitHub folder and no other folder shares the host
if (liveFolder.constructor.type === "github" && liveFolder.state.host) {
const host = liveFolder.state.host;
const otherFolderUsesHost = Array.from(this.liveFolders.values()).some(
f =>
f !== liveFolder &&
f.constructor.type === "github" &&
f.state.host === host
);
if (!otherFolderUsesHost) {
lazy.GithubTokenManager.removeToken(host).catch(() => {});
}
}

this.liveFolders.delete(id);

const prefix = `${id}:`;
Expand Down Expand Up @@ -502,6 +534,15 @@ class nsZenLiveFoldersManager {

// Helpers
// -------
hasGitHubLiveFolder() {
for (const liveFolder of this.liveFolders.values()) {
if (liveFolder.constructor.type === "github") {
return true;
}
}
return false;
}

#applyDefaultStateValues(state) {
state.interval ||= DEFAULT_FETCH_INTERVAL;
state.lastFetched ||= 0;
Expand Down
1 change: 1 addition & 0 deletions src/zen/live-folders/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

EXTRA_JS_MODULES.zen += [
"providers/GithubAuth.sys.mjs",
"providers/GithubLiveFolder.sys.mjs",
"providers/RssLiveFolder.sys.mjs",
"ZenLiveFolder.sys.mjs",
Expand Down
115 changes: 115 additions & 0 deletions src/zen/live-folders/providers/GithubAuth.sys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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/.

const lazy = {};

ChromeUtils.defineLazyGetter(lazy, "l10n", () => new Localization(["browser/zen-live-folders.ftl"]));

const LoginInfo = Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo,
"init"
);

export class GithubTokenManager {
static REALM = "zen-live-folder-github-pat";

/**
* Returns the stored PAT for the given origin, or null if none exists.
*
* @param {string} origin - The GitHub host origin (e.g. "https://github.com")
* @returns {Promise<string|null>}
*/
static async getToken(origin) {
const logins = await Services.logins.searchLoginsAsync({
origin,
httpRealm: GithubTokenManager.REALM,
});
if (logins.length > 0) {
return logins[0].password;
}
return null;
}

/**
* Stores or updates a PAT for the given origin.
*
* @param {string} origin - The GitHub host origin
* @param {string} token - The personal access token
*/
static async setToken(origin, token) {
const logins = await Services.logins.searchLoginsAsync({
origin,
httpRealm: GithubTokenManager.REALM,
});

if (logins.length > 0) {
const oldLogin = logins[0];
const newLoginData = oldLogin.clone();
newLoginData.password = token;
await Services.logins.modifyLoginAsync(oldLogin, newLoginData);
} else {
const loginInfo = new LoginInfo(
origin,
null, // formActionOrigin
GithubTokenManager.REALM,
"", // username
token,
"", // usernameField
"" // passwordField
);
await Services.logins.addLoginAsync(loginInfo);
}
}

/**
* Removes the stored PAT for the given origin.
*
* @param {string} origin - The GitHub host origin
*/
static async removeToken(origin) {
const logins = await Services.logins.searchLoginsAsync({
origin,
httpRealm: GithubTokenManager.REALM,
});
if (logins.length > 0) {
await Services.logins.removeLoginAsync(logins[0]);
}
}

/**
* Shows a password prompt for the user to enter a PAT, validates it,
* stores it if valid, and returns whether a token was successfully stored.
*
* @param {Window} window - The browser window for the prompt
* @param {string} origin - The GitHub host origin
* @returns {Promise<boolean>}
*/
static async promptForToken(window, origin) {
const title = await lazy.l10n.formatValue("zen-live-folder-github-prompt-token");
const passwordObj = { value: "" };
const checkObj = { value: false };

const ok = Services.prompt.promptPassword(
window,
title,
title,
passwordObj,
null,
checkObj
);

if (!ok) {
return false;
}

const token = passwordObj.value.trim();
if (!token) {
return false;
}

await GithubTokenManager.setToken(origin, token);
return true;
}
}
Loading