Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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