Implement options page (#38)

This commit is contained in:
Maxim Baz
2019-03-11 12:33:24 +01:00
committed by GitHub
parent b1abed2f2b
commit 0b908f8ef9
9 changed files with 435 additions and 34 deletions

View File

@@ -8,15 +8,18 @@ extension:
$(MAKE) -C src
CHROME_FILES := manifest.json \
*.css \
*.png \
popup/*.html \
popup/*.svg
*.css \
*.png \
popup/*.html \
popup/*.svg \
options/*.html
CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \
src/css/popup.dist.css \
src/js/background.dist.js \
src/js/popup.dist.js \
src/js/inject.dist.js
src/css/popup.dist.css \
src/css/options.dist.css \
src/js/background.dist.js \
src/js/popup.dist.js \
src/js/options.dist.js \
src/js/inject.dist.js
CHROME_FILES := $(patsubst src/%,chrome/%,$(CHROME_FILES))
.PHONY: chrome

View File

@@ -3,10 +3,10 @@ PRETTIER := node_modules/.bin/prettier
LESSC := node_modules/.bin/lessc
CLEAN_FILES := js
PRETTIER_FILES := $(wildcard *.js popup/*.js *.less popup/*.less)
PRETTIER_FILES := $(wildcard *.json *.js popup/*.js options/*.js *.less popup/*.less options/*.less *.html popup/*.html options/*.html)
.PHONY: all
all: deps prettier css/popup.dist.css js/background.dist.js js/popup.dist.js js/inject.dist.js
all: deps prettier css/popup.dist.css css/options.dist.css js/background.dist.js js/popup.dist.js js/options.dist.js js/inject.dist.js
.PHONY: deps
deps:
@@ -20,6 +20,10 @@ css/popup.dist.css: $(LESSC) popup/popup.less
[ -d css ] || mkdir -p css
$(LESSC) popup/popup.less css/popup.dist.css
css/options.dist.css: $(LESSC) options/options.less
[ -d css ] || mkdir -p css
$(LESSC) options/options.less css/options.dist.css
js/background.dist.js: $(BROWSERIFY) background.js
[ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/background.dist.js background.js
@@ -28,6 +32,10 @@ js/popup.dist.js: $(BROWSERIFY) popup/*.js
[ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/popup.dist.js popup/popup.js
js/options.dist.js: $(BROWSERIFY) options/*.js
[ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/options.dist.js options/options.js
js/inject.dist.js: $(BROWSERIFY) inject.js
[ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/inject.dist.js inject.js

View File

@@ -125,18 +125,18 @@ async function dispatchFill(
// if user answered a foreign-origin confirmation,
// store the answers in the settings
var needSaveSettings = false;
var foreignFillsChanged = false;
for (var frame of perFrameFillResults) {
if (typeof frame.foreignFill !== "undefined") {
if (typeof settings.foreignFills[settings.host] === "undefined") {
settings.foreignFills[settings.host] = {};
}
settings.foreignFills[settings.host][frame.foreignOrigin] = frame.foreignFill;
needSaveSettings = true;
foreignFillsChanged = true;
}
}
if (needSaveSettings) {
saveSettings(settings);
if (foreignFillsChanged) {
await saveSettings(settings);
}
return filledFields;
@@ -328,12 +328,22 @@ async function handleMessage(settings, message, sendResponse) {
});
break;
case "saveSettings":
saveSettings(message.settings);
sendResponse({ status: "ok" });
try {
await saveSettings(message.settings);
sendResponse({ status: "ok" });
} catch (e) {
sendResponse({
status: "error",
message: "Unable to save settings" + e.toString()
});
}
break;
case "listFiles":
try {
var response = await hostAction(settings, "list");
if (response.status != "ok") {
throw new Error(JSON.stringify(response)); // TODO handle host error
}
sendResponse({ status: "ok", files: response.data.files });
} catch (e) {
sendResponse({
@@ -537,10 +547,10 @@ async function receiveMessage(message, sender, sendResponse) {
var configureSettings = Object.assign(deepCopy(settings), {
defaultStore: {}
});
var response = await chrome.runtime.sendNativeMessage(appID, {
settings: configureSettings,
action: "configure"
});
var response = await hostAction(configureSettings, "configure");
if (response.status != "ok") {
throw new Error(JSON.stringify(response)); // TODO handle host error
}
settings.version = response.version;
if (Object.keys(settings.stores).length > 0) {
// there are user-configured stores present
@@ -588,9 +598,7 @@ async function receiveMessage(message, sender, sendResponse) {
try {
settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
settings.host = new URL(settings.tab.url).hostname;
} catch (e) {
throw new Error("Unable to retrieve current tab information");
}
} catch (e) {}
handleMessage(settings, message, sendResponse);
} catch (e) {
@@ -601,17 +609,22 @@ async function receiveMessage(message, sender, sendResponse) {
}
/**
* Save settings
* Save settings if they are valid
*
* @since 3.0.0
*
* @param object Final settings object
* @return void
*/
function saveSettings(settings) {
async function saveSettings(settings) {
// 'default' is our reserved name for the default store
delete settings.stores.default;
var response = await hostAction(settings, "configure");
if (response.status != "ok") {
throw new Error(JSON.stringify(response)); // TODO handle host error
}
for (var key in defaultSettings) {
if (settings.hasOwnProperty(key)) {
localStorage.setItem(key, JSON.stringify(settings[key]));

View File

@@ -1,19 +1,14 @@
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlVUvevBvdeIFpvK5Xjcbd/cV8AsMNLg0Y7BmUetSTagjts949Tp12mNmWmIEEaE9Zwmfjl1ownWiclGhsoPSf6x7nP/i0j8yROv6TYibXLhZet9y4vnUMgtCIkb3O5RnuOl0Y+V3XUADwxotmgT1laPUThymJoYnWPv+lwDkYiEopX2Aq2amzRj8aMogNBUbAIkCMxfa9WK3Vm0QTAUdV4ii9WqzbgjHVruQpiFVq99W2U9ddsWNZjOG/36sFREuHw+reulQgblp9FZdaN1Q9X5cGcT5bncQIRB6K3wZYa805gFENc93Wslmzu6aUSEKqqPymlI5ikedaPlXPmlqwIDAQAB",
"manifest_version": 2,
"name": "Browserpass CE",
"description": "Browser extension for zx2c4's pass (password manager) - Community Edition.",
"name": "Browserpass",
"description": "Browser extension for zx2c4's pass (password manager)",
"version": "3.0.0",
"author": [
"Maxim Baz <browserpass@maximbaz.com>",
"Steve Gilberd <steve@erayd.net>"
],
"author": ["Maxim Baz <browserpass@maximbaz.com>", "Steve Gilberd <steve@erayd.net>"],
"homepage_url": "https://github.com/browserpass/browserpass-extension",
"background": {
"persistent": true,
"scripts": [
"js/background.dist.js"
]
"scripts": ["js/background.dist.js"]
},
"icons": {
"128": "icon-lock.png"
@@ -22,6 +17,11 @@
"default_icon": "icon-lock.png",
"default_popup": "popup/popup.html"
},
"options_ui": {
"page": "options/options.html",
"chrome_style": true,
"open_in_tab": false
},
"permissions": [
"activeTab",
"nativeMessaging",

193
src/options/interface.js Normal file
View File

@@ -0,0 +1,193 @@
module.exports = Interface;
var m = require("mithril");
/**
* Options main interface
*
* @since 3.0.0
*
* @param object settings Settings object
* @param function saveSettings Function to save settings
* @return void
*/
function Interface(settings, saveSettings) {
// public methods
this.attach = attach;
this.view = view;
// fields
this.settings = settings;
this.saveSettings = saveSettings;
this.saveEnabled = false;
// 'default' store must not be displayed or later attempted to be saved
delete this.settings.stores.default;
}
/**
* Attach the interface on the given element
*
* @since 3.0.0
*
* @param DOMElement element Target element
* @return void
*/
function attach(element) {
m.mount(element, this);
}
/**
* Generates vnodes for render
*
* @since 3.0.0
*
* @param function ctl Controller
* @param object params Runtime params
* @return []Vnode
*/
function view(ctl, params) {
var nodes = [];
nodes.push(m("h3", "Basic settings"));
nodes.push(createCheckbox.call(this, "autoSubmit", "Automatically submit forms after filling"));
nodes.push(m("h3", "Custom store locations"));
nodes.push(
m("div", { class: "notice" }, "(this overrides default store and $PASSWORD_STORE_DIR)")
);
for (var storeId in this.settings.stores) {
nodes.push(createCustomStore.call(this, storeId));
}
nodes.push(
m(
"a.add-store",
{
onclick: () => {
addEmptyStore(this.settings.stores);
this.saveEnabled = true;
}
},
"Add store"
)
);
if (typeof this.error !== "undefined") {
nodes.push(m("div.error", this.error.message));
}
nodes.push(
m(
"button.save",
{
disabled: !this.saveEnabled,
onclick: async () => {
try {
await this.saveSettings(this.settings);
this.error = undefined;
} catch (e) {
this.error = e;
}
this.saveEnabled = false;
m.redraw();
}
},
"Save"
)
);
return nodes;
}
/**
* Generates vnode for a checkbox setting
*
* @since 3.0.0
*
* @param string key Settings key
* @param string title Label for the checkbox
* @return Vnode
*/
function createCheckbox(key, title) {
return m("div.option", { class: key }, [
m("label", [
m("input[type=checkbox]", {
title: title,
checked: this.settings[key],
onchange: e => {
this.settings[key] = e.target.checked;
this.saveEnabled = true;
}
}),
title
])
]);
}
/**
* Generates vnode for a custom store configuration
*
* @since 3.0.0
*
* @param string storeId Store ID
* @return Vnode
*/
function createCustomStore(storeId) {
let store = this.settings.stores[storeId];
return m("div.option.custom-store", { class: "store-" + store.name }, [
m("input[type=text].name", {
title: "The name for this password store",
value: store.name,
placeholder: "name",
onchange: e => {
store.name = e.target.value;
this.saveEnabled = true;
}
}),
m("input[type=text].path", {
title: "The full path to this password store",
value: store.path,
placeholder: "/path/to/store",
onchange: e => {
store.path = e.target.value;
this.saveEnabled = true;
}
}),
m(
"a.remove",
{
title: "Remove this password store",
onclick: () => {
delete this.settings.stores[storeId];
this.saveEnabled = true;
}
},
"[X]"
)
]);
}
/**
* Generates new store ID
*
* @since 3.0.0
*
* @return string new store ID
*/
function newId() {
return Math.random()
.toString(36)
.substr(2, 9);
}
/**
* Generates a new empty store
*
* @since 3.0.0
*
* @param []object stores List of stores to add a new store to
* @return void
*/
function addEmptyStore(stores) {
let store = { id: newId(), name: "", path: "" };
stores[store.id] = store;
}

10
src/options/options.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="../css/options.dist.css" />
<script src="../js/options.dist.js"></script>
</head>
<body>
<div>Loading options...</div>
</body>
</html>

66
src/options/options.js Normal file
View File

@@ -0,0 +1,66 @@
//------------------------------------- Initialisation --------------------------------------//
"use strict";
require("chrome-extension-async");
var Interface = require("./interface");
run();
//----------------------------------- Function definitions ----------------------------------//
/**
* Handle an error
*
* @since 3.0.0
*
* @param Error error Error object
* @param string type Error type
*/
function handleError(error, type = "error") {
if (type == "error") {
console.log(error);
}
var errorNode = document.createElement("div");
errorNode.setAttribute("class", "part " + type);
errorNode.textContent = error.toString();
document.body.innerHTML = "";
document.body.appendChild(errorNode);
}
/**
* Save settings
*
* @since 3.0.0
*
* @param object settings Settings object
*/
async function saveSettings(settings) {
var response = await chrome.runtime.sendMessage({
action: "saveSettings",
settings: settings
});
if (response.status != "ok") {
throw new Error(response.message);
}
}
/**
* Run the main options logic
*
* @since 3.0.0
*
* @return void
*/
async function run() {
try {
var response = await chrome.runtime.sendMessage({ action: "getSettings" });
if (response.status != "ok") {
throw new Error(response.message);
}
var options = new Interface(response.settings, saveSettings);
options.attach(document.body);
} catch (e) {
handleError(e);
}
}

104
src/options/options.less Normal file
View File

@@ -0,0 +1,104 @@
html,
body {
box-sizing: border-box;
overflow: hidden;
margin: 0;
}
body {
margin: 10px 20px 20px;
}
h3 {
margin-top: 20px;
}
h3:first-child {
margin-top: 0;
}
.notice {
margin-top: -10px;
color: gray;
font-size: 10px;
}
.option {
display: flex;
height: 16px;
line-height: 16px;
margin-bottom: 10px;
margin-top: 10px;
}
.option input[type="checkbox"] {
height: 12px;
margin: 2px 6px 2px 0;
padding: 0;
}
.option.custom-store input[type="text"] {
background-color: white;
color: black;
border: none;
border-bottom: 1px solid #aaa;
height: 16px;
line-height: 16px;
margin: -4px 0 0 0;
overflow: hidden;
padding: 0;
width: 25%;
}
.option.custom-store input[type="text"].path {
margin-left: 6px;
width: calc(100% - 25% - 48px);
}
.option.custom-store a.remove {
color: #f00;
display: block;
height: 16px;
line-height: 16px;
margin: 0 0 0 6px;
padding: 0;
text-decoration: none;
width: 16px;
cursor: pointer;
}
.add-store {
cursor: pointer;
display: block;
margin-top: 8px;
margin-bottom: 30px;
}
.error {
margin-bottom: 30px;
color: red;
}
.save {
cursor: pointer;
display: block;
}
@-moz-document url-prefix() {
body {
background: #fff;
border: 1px solid #000;
font-family: sans;
margin: 2px;
padding: 12px;
}
.option.custom-store input[type="text"] {
background: #fff;
margin: 2px;
}
.option.custom-store a.remove {
font-size: 12px;
}
}

View File

@@ -67,6 +67,10 @@ async function run() {
}
var settings = response.settings;
if (typeof settings.host === "undefined") {
throw new Error("Unable to retrieve current tab information");
}
// get list of logins
response = await chrome.runtime.sendMessage({ action: "listFiles" });
if (response.status != "ok") {