Implement options page (#38)
This commit is contained in:
19
Makefile
19
Makefile
@@ -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
|
||||
|
12
src/Makefile
12
src/Makefile
@@ -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
|
||||
|
@@ -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]));
|
||||
|
@@ -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
193
src/options/interface.js
Normal 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
10
src/options/options.html
Normal 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
66
src/options/options.js
Normal 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
104
src/options/options.less
Normal 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;
|
||||
}
|
||||
}
|
@@ -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") {
|
||||
|
Reference in New Issue
Block a user