First pass at implementing browser extension with auto-fill

This commit is contained in:
Martin Kleinschrodt 2020-01-09 17:12:45 +01:00
parent 3b0048ec32
commit 5aee5d79d0
22 changed files with 661 additions and 90 deletions

View File

@ -2,53 +2,48 @@
font-family: "Nunito";
font-style: normal;
font-weight: 300;
src: local("Nunito Light"), local("Nunito-Light"), url(../../assets/fonts/Nunito-Light.ttf) format("truetype");
src: local("Nunito Light"), local("Nunito-Light"), url(./Nunito-Light.ttf) format("truetype");
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: local("Nunito Regular"), local("Nunito-Regular"), url(../../assets/fonts/Nunito-Regular.ttf) format("truetype");
src: local("Nunito Regular"), local("Nunito-Regular"), url(./Nunito-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 600;
src: local("Nunito SemiBold"), local("Nunito-SemiBold"),
url(../../assets/fonts/Nunito-SemiBold.ttf) format("truetype");
src: local("Nunito SemiBold"), local("Nunito-SemiBold"), url(./Nunito-SemiBold.ttf) format("truetype");
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 700;
src: local("Nunito Bold"), local("Nunito-Bold"), url(../../assets/fonts/Nunito-Bold.ttf) format("truetype");
src: local("Nunito Bold"), local("Nunito-Bold"), url(./Nunito-Bold.ttf) format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 400;
src: local("Inconsolata Regular"), local("Inconsolata-Regular"),
url(../../assets/fonts/Inconsolata-Regular.ttf) format("truetype");
src: local("Inconsolata Regular"), local("Inconsolata-Regular"), url(./Inconsolata-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Padlock";
src: url("../../assets/fonts/Padlock.eot");
src: url("../../assets/fonts/Padlock.eot") format("embedded-opentype"),
url("../../assets/fonts/Padlock.woff") format("woff"), url("../../assets/fonts/Padlock.ttf") format("truetype"),
url("../../assets/fonts/Padlock.svg#Padlock") format("svg");
src: url("./Padlock.eot");
src: url("./Padlock.eot") format("embedded-opentype"), url("./Padlock.woff") format("woff"),
url("./Padlock.ttf") format("truetype"), url("./Padlock.svg#Padlock") format("svg");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "FontAwesome";
src: url("../../assets/fonts/fontawesome-webfont.eot");
src: url("../../assets/fonts/fontawesome-webfont.eot") format("embedded-opentype"),
url("../../assets/fonts/fontawesome-webfont.woff2") format("woff2"),
url("../../assets/fonts/fontawesome-webfont.woff") format("woff"),
url("../../assets/fonts/fontawesome-webfont.ttf") format("truetype"),
url("../../assets/fonts/fontawesome-webfont.svg") format("svg");
src: url("./fontawesome-webfont.eot");
src: url("./fontawesome-webfont.eot") format("embedded-opentype"),
url("./fontawesome-webfont.woff2") format("woff2"), url("./fontawesome-webfont.woff") format("woff"),
url("./fontawesome-webfont.ttf") format("truetype"), url("./fontawesome-webfont.svg") format("svg");
font-weight: normal;
font-style: normal;
}

View File

@ -43,8 +43,12 @@ export class App extends ServiceWorker(StateMixin(AutoSync(ErrorHandling(AutoLoc
@property({ type: Boolean, reflect: true, attribute: "singleton-container" })
readonly singletonContainer = true;
get router() {
return router;
}
@property()
private _ready = false;
protected _ready = false;
@query("pl-start")
private _startView: Start;
@ -97,6 +101,10 @@ export class App extends ServiceWorker(StateMixin(AutoSync(ErrorHandling(AutoLoc
async load() {
await app.loaded;
// Try syncing account so user can unlock with new password in case it has changed
if (app.state.loggedIn) {
app.fetchAccount();
}
this._ready = true;
this._routeChanged();
const spinner = document.querySelector(".spinner") as HTMLElement;
@ -343,11 +351,20 @@ export class App extends ServiceWorker(StateMixin(AutoSync(ErrorHandling(AutoLoc
this._routeChanged();
}
protected _unlocked() {
setTimeout(() => {
this.$(".wrapper").classList.add("active");
router.go(router.params.next || "", {}, true);
}, 600);
protected _unlocked(instant = false) {
setTimeout(
async () => {
if (!this.$(".wrapper")) {
await this.updateComplete;
}
this.$(".wrapper").classList.add("active");
if (typeof router.params.next !== "undefined") {
router.go(router.params.next, {}, true);
}
},
instant ? 0 : 600
);
}
protected _loggedIn() {}

View File

@ -657,6 +657,7 @@ export class ItemsList extends StateMixin(View) {
fieldEl.classList.add("copied");
setTimeout(() => fieldEl.classList.remove("copied"), 1000);
app.updateItem(vault, item, { lastUsed: new Date() });
this.dispatch("auto-fill", { item, index });
}
private _openAttachment(a: AttachmentInfo, item: VaultItem, e: MouseEvent) {

View File

@ -14,10 +14,6 @@ export class Login extends StartForm {
@property()
private _errorMessage: string;
private get _email() {
return router.params.email || "";
}
@query("#emailInput")
private _emailInput: Input;
@query("#passwordInput")
@ -27,12 +23,18 @@ export class Login extends StartForm {
private _failedCount = 0;
private _verificationToken?: string;
async reset() {
await this.updateComplete;
this._emailInput.value = router.params.email || "";
this._passwordInput.value = "";
this._loginButton.stop();
this._failedCount = 0;
super.reset();
if (router.params.verifying) {
this._verifyEmail();
}
}
static styles = [
@ -70,7 +72,6 @@ export class Login extends StartForm {
required
select-on-focus
.label=${$l("Email Address")}
.value=${this._email}
class="animate tap"
@enter=${() => this._submit()}
>
@ -105,7 +106,41 @@ export class Login extends StartForm {
`;
}
private async _submit(verificationToken?: string): Promise<void> {
private async _verifyEmail() {
router.params = { ...router.params, email: this._emailInput.value, verifying: "1" };
const verify = await prompt(
$l("Please enter the confirmation code sent to your email address to proceed!"),
{
title: $l("One Last Step!"),
placeholder: $l("Enter Verification Code"),
confirmLabel: $l("Submit"),
type: "number",
pattern: "[0-9]*",
validate: async (code: string) => {
try {
return await app.completeEmailVerification(this._emailInput.value, code);
} catch (e) {
if (e.code === ErrorCode.EMAIL_VERIFICATION_TRIES_EXCEEDED) {
alert($l("Maximum number of tries exceeded! Please resubmit and try again!"), {
type: "warning"
});
return "";
}
throw e.message || e.code || e.toString();
}
}
}
);
if (verify) {
this._verificationToken = verify;
}
return verify;
}
private async _submit(): Promise<void> {
if (this._loginButton.state === "loading") {
return;
}
@ -137,7 +172,7 @@ export class Login extends StartForm {
this._errorMessage = "";
this._loginButton.start();
try {
await app.login(email, password, verificationToken);
await app.login(email, password, this._verificationToken);
this._loginButton.success();
this.done();
} catch (e) {
@ -146,30 +181,9 @@ export class Login extends StartForm {
this._loginButton.stop();
await app.requestEmailVerification(email);
const verify = await this._verifyEmail();
const verify = await prompt(
$l("Please enter the confirmation code sent to your email address to proceed!"),
{
title: $l("One Last Step!"),
placeholder: $l("Enter Verification Code"),
confirmLabel: $l("Submit"),
validate: async (code: string) => {
try {
return await app.completeEmailVerification(email, code);
} catch (e) {
if (e.code === ErrorCode.EMAIL_VERIFICATION_TRIES_EXCEEDED) {
alert($l("Maximum number of tries exceeded! Please resubmit and try again!"), {
type: "warning"
});
return "";
}
throw e.message || e.code || e.toString();
}
}
}
);
return verify ? this._submit(verify) : undefined;
return verify ? this._submit() : undefined;
case ErrorCode.INVALID_CREDENTIALS:
this._errorMessage = $l("Wrong username or password. Please try again!");
this._loginButton.fail();

View File

@ -15,6 +15,7 @@ export interface PromptOptions {
placeholder?: string;
label?: string;
type?: string;
pattern?: string;
confirmLabel?: string;
cancelLabel?: string;
preventDismiss?: boolean;
@ -41,6 +42,8 @@ export class PromptDialog extends Dialog<PromptOptions, string | null> {
@property({ reflect: true })
type: string = defaultType;
@property()
pattern: string = "";
@property()
validate?: (val: string, input: Input) => Promise<string>;
@property()
private _validationMessage: string = "";
@ -92,6 +95,7 @@ export class PromptDialog extends Dialog<PromptOptions, string | null> {
.type=${this.type}
.placeholder=${this.placeholder}
.label=${this.label}
.pattern=${this.pattern}
@enter=${() => this._confirmButton.click()}
>
</pl-input>
@ -131,6 +135,7 @@ export class PromptDialog extends Dialog<PromptOptions, string | null> {
label = "",
value = "",
type = defaultType,
pattern = "",
confirmLabel = defaultConfirmLabel,
cancelLabel = defaultCancelLabel,
preventDismiss = true,
@ -139,6 +144,7 @@ export class PromptDialog extends Dialog<PromptOptions, string | null> {
this.title = title;
this.message = message;
this.type = type;
this.pattern = pattern;
this.placeholder = placeholder;
this.label = label;
this.confirmLabel = confirmLabel;

View File

@ -44,6 +44,7 @@ export class Router extends EventEmitter {
"",
this.basePath + this.path + "?" + new URLSearchParams(params).toString()
);
this.dispatch("params-changed", {params});
}
get canGoBack() {

View File

@ -5,5 +5,6 @@ interface Navigator {
interface Window {
app: any;
router: any;
extension: any;
getPlatform: any;
}

View File

@ -323,11 +323,6 @@ export class App {
// Save back to storage
await this.storage.save(this.state);
// Try syncing account so user can unlock with new password in case it has changed
if (this.account) {
this.fetchAccount();
}
this.loadBillingProvider();
// Notify state change

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -612,6 +612,11 @@
"@xtuc/long": "4.2.2"
}
},
"@webcomponents/webcomponentsjs": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.1.tgz",
"integrity": "sha512-7jxBb+KoWncKb/JGFyTY40PjV4yRx2zd35ZLuvRP+6WndJDL7X32ZIZ7bN3sSQIl+NzJkCo7chfXJyzn+6WZaQ=="
},
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -4290,6 +4295,19 @@
"invert-kv": "^2.0.0"
}
},
"lit-element": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.2.1.tgz",
"integrity": "sha512-ipDcgQ1EpW6Va2Z6dWm79jYdimVepO5GL0eYkZrFvdr0OD/1N260Q9DH+K5HXHFrRoC7dOg+ZpED2XE0TgGdXw==",
"requires": {
"lit-html": "^1.0.0"
}
},
"lit-html": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.1.2.tgz",
"integrity": "sha512-FFlUMKHKi+qG1x1iHNZ1hrtc/zHmfYTyrSvs3/wBTvaNtpZjOZGWzU7efGYVpgp6KvWeKF6ql9/KsCq6Z/mEDA=="
},
"load-bmfont": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.0.tgz",

View File

@ -18,6 +18,8 @@
"dependencies": {
"@padloc/app": "^3.0.13",
"@padloc/core": "^3.0.13",
"@webcomponents/webcomponentsjs": "^2.4.1",
"lit-element": "^2.2.1",
"webextension-polyfill-ts": "^0.11.0"
},
"devDependencies": {
@ -30,7 +32,7 @@
"style-loader": "^1.0.0",
"ts-loader": "^6.0.4",
"ts-node": "^7.0.1",
"typescript": "^3.5.2",
"typescript": "^3.7.4",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.5"
},

View File

@ -2,6 +2,8 @@ import { browser } from "webextension-polyfill-ts";
import { App } from "@padloc/app/src/elements/app";
import { debounce } from "@padloc/core/src/util";
import { bytesToBase64, base64ToBytes } from "@padloc/core/src/encoding";
import { Storable } from "@padloc/core/src/storage";
import { VaultItem } from "@padloc/core/src/item";
const notifyStateChanged = debounce(() => {
browser.runtime.sendMessage({
@ -9,15 +11,40 @@ const notifyStateChanged = debounce(() => {
});
}, 500);
class RouterState extends Storable {
id = "";
path = "";
params: { [key: string]: string } = {};
constructor(vals: Partial<RouterState>) {
super();
Object.assign(this, vals);
}
}
export class ExtensionApp extends App {
async load() {
await this.app.load();
if (this.locked) {
const masterKey = await browser.runtime.sendMessage({ type: "requestMasterKey" });
if (masterKey) {
this.app.unlockWithMasterKey(base64ToBytes(masterKey));
await this.app.unlockWithMasterKey(base64ToBytes(masterKey));
this._ready = true;
this._unlocked(true);
}
}
try {
const routerState = await this.app.storage.get(RouterState, "");
this.router.go(routerState.path, routerState.params, true);
} catch (e) {}
this.router.addEventListener("route-changed", () => this._saveRouterState());
this.router.addEventListener("params-changed", () => this._saveRouterState());
this.addEventListener("auto-fill", (e: any) => this._autoFill(e));
return super.load();
}
@ -26,8 +53,8 @@ export class ExtensionApp extends App {
notifyStateChanged();
}
_unlocked() {
super._unlocked();
_unlocked(instant = false) {
super._unlocked(instant);
if (!this.state.account || !this.state.account.masterKey) {
return;
}
@ -57,6 +84,31 @@ export class ExtensionApp extends App {
type: "loggedOut"
});
}
private async _saveRouterState() {
await this.app.storage.save(new RouterState({ path: this.router.path, params: this.router.params }));
}
private async _autoFill({ detail: { item, index } }: CustomEvent<{ item: VaultItem; index: number }>) {
const [activeTab] = await browser.tabs.query({ active: true });
if (activeTab) {
let contentReady = false;
try {
contentReady = await browser.tabs.sendMessage(activeTab.id!, { type: "isContentReady" });
} catch (e) {}
if (!contentReady) {
await browser.tabs.executeScript(activeTab.id, { file: "/content.js" });
}
browser.tabs.sendMessage(activeTab.id!, {
type: "autoFill",
item,
index
});
window.close();
}
}
}
customElements.define("pl-extension-app", ExtensionApp);

View File

@ -1,14 +1,15 @@
import { browser } from "webextension-polyfill-ts";
import { setPlatform } from "@padloc/core/src/platform";
import { App } from "@padloc/core/src/app";
import { bytesToBase64, base64ToBytes } from "@padloc/core/src/encoding";
import { bytesToBase64, base64ToBytes, base32ToBytes } from "@padloc/core/src/encoding";
import { AjaxSender } from "@padloc/app/src/lib/ajax";
import { totp } from "@padloc/core/src/otp";
import { ExtensionPlatform } from "./platform";
import { Message } from "./message";
setPlatform(new ExtensionPlatform());
class Extension {
class ExtensionBackground {
app = new App(new AjaxSender(process.env.PL_SERVER_URL!));
async init() {
@ -25,6 +26,8 @@ class Extension {
break;
case "requestMasterKey":
return this.app.account && this.app.account.masterKey && bytesToBase64(this.app.account.masterKey) || null;
case "calcTOTP":
return totp(base32ToBytes(msg.secret));
}
});
}
@ -36,7 +39,7 @@ class Extension {
private _updateIcon() {
if (!this.app.account) {
browser.browserAction.setIcon({ path: "icon-warning.png" });
browser.browserAction.setIcon({ path: "icon-locked.png" });
browser.browserAction.setTitle({ title: "Please Log In" });
} else {
browser.browserAction.setIcon({ path: "icon.png" });
@ -46,7 +49,7 @@ class Extension {
}
//@ts-ignore
const extension = (window.extension = new Extension());
const extension = (window.extension = new ExtensionBackground());
extension.init();

View File

@ -0,0 +1,100 @@
import "@webcomponents/webcomponentsjs";
import { browser } from "webextension-polyfill-ts";
// import { FieldType, FIELD_DEFS } from "@padloc/core/src/item";
import { ExtensionToolbar } from "./toolbar";
import "./toolbar";
import { Message } from "./message";
// export interface AutoFillableInput {
// type: FieldType;
// element: HTMLInputElement;
// }
const css = `
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: url("${browser.runtime.getURL("Nunito-Regular.ttf")}") format("truetype");
}
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 600;
src: url("${browser.runtime.getURL("Nunito-SemiBold.ttf")}") format("truetype");
}
@font-face {
font-family: "FontAwesome";
src: url("${browser.runtime.getURL("fontawesome-webfont.ttf")}") format("truetype");
font-weight: normal;
font-style: normal;
}
@keyframes ripple {
from {
opacity: 0.3;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(2);
}
}
.ripple {
position: absolute;
z-index: 9999999;
border-radius: 8px;
background: #3bb7f9;
animation: ripple 1s both;
pointer-events: none;
will-change: transform, opacity;
}
`;
class ExtensionContent {
private _toolbar?: ExtensionToolbar;
// findAutoFillableElements() {
// const elements: AutoFillableInput[] = [];
// for (const [type, { selector }] of Object.entries(FIELD_DEFS)) {
// elements.push(
// ...Array.from(document.querySelectorAll(selector as string)).map(el => ({
// type: type as FieldType,
// element: el as HTMLInputElement
// }))
// );
// }
// return elements;
// }
async init() {
const style = document.createElement("style");
document.head.appendChild(style);
style.type = "text/css";
style.appendChild(document.createTextNode(css));
this._toolbar = document.createElement("pl-extension-toolbar") as ExtensionToolbar;
document.body.appendChild(this._toolbar);
browser.runtime.onMessage.addListener((msg: Message) => this._handleMessage(msg));
}
private _handleMessage(msg: Message) {
switch (msg.type) {
case "autoFill":
this._toolbar!.open(msg.item, msg.index);
return Promise.resolve(true);
case "isContentReady":
return Promise.resolve(true);
}
}
}
if (typeof window.extension === "undefined") {
window.extension = new ExtensionContent();
window.extension.init();
}

View File

@ -0,0 +1,24 @@
{
"name": "Padloc",
"version": "1.0.0",
"description": "Padloc Browser Extension",
"manifest_version": 2,
"icons": {
"128": "icon.png"
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"web_accessible_resources": [
"icon.png",
"Nunito-Regular.ttf",
"Nunito-SemiBold.ttf",
"fontawesome-webfont.ttf"
],
"permissions": ["storage", "unlimitedStorage", "activeTab"]
}

View File

@ -1,3 +1,5 @@
import { VaultItem } from "@padloc/core/src/item";
export type Message =
| {
type: "loggedIn";
@ -12,4 +14,7 @@ export type Message =
type: "unlocked";
masterKey: string;
}
| { type: "requestMasterKey" };
| { type: "requestMasterKey" }
| { type: "autoFill"; item: VaultItem; index: number }
| { type: "calcTOTP"; secret: string }
| { type: "isContentReady" };

View File

@ -2,6 +2,7 @@ import { setPlatform } from "@padloc/core/src/platform";
import { ExtensionPlatform } from "./platform";
import "../assets/icon.png";
import "../assets/icon-warning.png";
import "../assets/icon-locked.png";
(async () => {
setPlatform(new ExtensionPlatform());

View File

@ -0,0 +1,330 @@
import { browser } from "webextension-polyfill-ts";
// import { totp } from "@padloc/core/src/otp";
// import { base32ToBytes } from "@padloc/core/src/encoding";
import { config } from "@padloc/app/src/styles";
import { BaseElement, html, property, css, element } from "@padloc/app/src/elements/base";
import { VaultItem } from "@padloc/core/src/item";
import "@padloc/app/src/elements/icon";
@element("pl-extension-toolbar")
export class ExtensionToolbar extends BaseElement {
@property()
item: VaultItem | null = null;
@property()
private _fieldIndex = 0;
private _lastFilledInput: HTMLInputElement | null = null;
static styles = [
config.cssVars,
css`
:host {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999999;
display: flex;
justify-content: center;
align-items: flex-end;
font-family: var(--font-family);
padding: 10px;
font-size: 14px;
color: var(--color-secondary);
pointer-events: none;
will-change: transform;
transform-origin: top right;
transition: transform 0.5s;
text-align: left;
}
:host(:not(.showing)) {
transform: scale(0);
transition: transform 0.5s;
}
.inner {
pointer-events: auto;
border-radius: var(--border-radius);
max-width: 100%;
background: var(--color-tertiary);
border: solid 1px #ddd;
border-bottom-width: 3px;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 20px;
padding: 4px;
}
.header {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.title {
flex: 1;
margin-right: 4px;
font-weight: 600;
font-size: 1.1em;
padding: 6px 6px 0 6px;
}
.hint {
opacity: 0.5;
font-size: 0.9em;
padding: 0 6px 6px 6px;
}
.fields {
display: flex;
overflow-x: auto;
}
.field-index {
background: rgba(0, 0, 0, 0.08);
border: solid 1px rgba(0, 0, 0, 0.1);
width: 1.5em;
height: 1.5em;
line-height: 1.6em;
border-radius: 4px;
border-bottom-width: 2px;
font-size: 0.7em;
margin-right: 0.5em;
}
button {
background: transparent;
color: inherit;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
border: none;
margin: 0;
padding: 6px;
cursor: pointer;
text-align: center;
outline: none;
border-radius: var(--border-radius);
display: flex;
align-items: center;
}
button:not(:last-child) {
margin-right: 4px;
}
button:hover:not([active]) {
background: #eee;
}
button[active] {
background: var(--color-primary);
color: var(--color-tertiary);
}
button.close {
padding: 0;
font-size: 0.9em;
border-radius: 100%;
width: 2em;
height: 2em;
line-height: 2em;
display: block;
}
button.close::before {
font-family: "FontAwesome";
content: "\\f00d";
}
`
];
constructor() {
super();
document.addEventListener("focusin", () => this._fillSelected());
document.addEventListener("keydown", (e: KeyboardEvent) => this._keydown(e));
this._fillSelected();
}
async open(item: VaultItem, index = 0) {
this.item = item;
this._fieldIndex = index;
await this.updateComplete;
this.classList.add("showing");
// this._fillNext();
}
close() {
this.classList.remove("showing");
setTimeout(() => (this.item = null), 500);
}
private _getActiveElement(doc: DocumentOrShadowRoot): Element | null {
const el = doc.activeElement;
return (el && el.shadowRoot && this._getActiveElement(el.shadowRoot)) || el;
}
private _isElementFillable(el: Element) {
return (
el instanceof HTMLInputElement &&
["text", "number", "email", "password", "tel", "date", "month", "search", "url"].includes(el.type)
);
}
private _getActiveInput(): HTMLInputElement | null {
const el = this._getActiveElement(document);
return el && this._isElementFillable(el) ? (el as HTMLInputElement) : null;
}
private async _fillSelected() {
const input = this._getActiveInput();
if (!this.item || input === this._lastFilledInput) {
return;
}
const filled = await this._fillIndex(this._fieldIndex);
if (filled) {
this._lastFilledInput = input;
this._fieldIndex = (this._fieldIndex + 1) % this.item.fields.length;
}
}
private async _fillIndex(index: number) {
const field = this.item && this.item.fields[index];
const input = this._getActiveInput();
if (!field || !input) {
return false;
}
// const value = field.type === "totp" ? await totp(base32ToBytes(field.value)) : field.value;
const value =
field.type === "totp"
? await browser.runtime.sendMessage({ type: "calcTOTP", secret: field.value })
: field.value;
input.value = value;
input.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
key: ""
})
);
input.dispatchEvent(
new KeyboardEvent("keyup", {
bubbles: true,
key: ""
})
);
input.dispatchEvent(
new KeyboardEvent("keypress", {
bubbles: true,
key: ""
})
);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
// setTimeout(() => input.blur(), 100);
// input.select();
// document.execCommand("paste");
const button = this.$(`.fields > :nth-child(${index + 1})`);
button && this._ripple(button);
this._ripple(input);
// setTimeout(() => this._ripple(input), 100);
// setTimeout(() => this._ripple(input), 200);
// this._fieldIndex = Math.min(this.item!.fields.length - 1, this._fieldIndex + 1);
return true;
}
private _ripple(el: HTMLElement) {
const { left, top, width, height } = el.getBoundingClientRect();
const ripple = document.createElement("div");
// const ripple = input.cloneNode(true) as HTMLElement;
Object.assign(ripple.style, {
left: left + "px",
top: top + "px",
width: width + "px",
height: height + "px"
});
ripple.classList.add("ripple");
document.body.appendChild(ripple);
setTimeout(() => document.body.removeChild(ripple), 500);
}
private _keydown({ code, ctrlKey, metaKey, altKey }: KeyboardEvent) {
if (code === "Escape") {
this.close();
}
if (!this.item) {
return;
}
const matchNumber = code.match(/Digit(\d)/);
const index = (matchNumber && parseInt(matchNumber[1])) || NaN;
if (!isNaN(index) && !!this.item.fields[index - 1]) {
const input = this._getActiveInput();
if ((ctrlKey || metaKey) && altKey && input) {
this._fillIndex(index - 1);
} else if (!input) {
this._fieldIndex = index - 1;
}
}
}
// private _move(e: MouseEvent) {
// console.log("move", e);
// }
//
// private _mousedown() {
// this.style.cursor = "grabbing";
// const handler = (e: MouseEvent) => this._move(e);
// document.addEventListener("mousemove", handler);
// document.addEventListener(
// "mouseup",
// () => {
// this.style.cursor = "";
// document.removeEventListener("mousemove", handler);
// },
// { once: true }
// );
// }
render() {
if (!this.item) {
return html``;
}
return html`
<div class="inner">
<div class="header">
<div class="title">
${this.item.name}
</div>
<button class="close" @click=${this.close}></button>
</div>
<div class="hint">Click the desired form input to fill!</div>
<div class="fields">
${this.item.fields.map(
(field, index) => html`
<button
class="field"
?active=${index === this._fieldIndex}
@click=${() => (this._fieldIndex = index)}
>
<div class="field-index">
${index + 1}
</div>
<div class="field-name">
${field.name}
</div>
</button>
`
)}
</div>
</div>
`;
}
}

View File

@ -1,3 +1,15 @@
{
"extends": "../app/tsconfig.json"
"extends": "../../tsconfig.json",
"compilerOptions": {
"strictPropertyInitialization": false,
"declaration": false,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"outDir": "dist",
"sourceMap": true,
"module": "esnext",
"strict": true
},
"include": ["src/**/*.ts", "../app/types/**/*.ts"],
"exclude": ["node_modules/**/*.ts"]
}

View File

@ -2,14 +2,16 @@ const path = require("path");
const { EnvironmentPlugin } = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { version, description } = require("./package.json");
const { version } = require("./package.json");
const manifest = require("./src/manifest.json");
const serverUrl = process.env.PL_SERVER_URL || `http://0.0.0.0:${process.env.PL_SERVER_PORT || 3000}`;
module.exports = {
entry: {
popup: path.resolve(__dirname, "src/popup.ts"),
background: path.resolve(__dirname, "src/background.ts")
background: path.resolve(__dirname, "src/background.ts"),
content: path.resolve(__dirname, "src/content.ts")
},
output: {
path: path.resolve(__dirname, "dist"),
@ -74,22 +76,8 @@ module.exports = {
compiler.hooks.emit.tap("Web Extension Manifest", compilation => {
const jsonString = JSON.stringify(
{
name: "Padloc",
version,
description,
manifest_version: 2,
icons: {
"128": "icon.png"
},
background: {
scripts: ["background.js"],
persistent: false
},
browser_action: {
default_popup: "popup.html",
default_icon: "icon.png"
},
permissions: ["storage", "unlimitedStorage"]
...manifest,
version
},
null,
4

View File

@ -67,7 +67,5 @@
<svg viewBox="0 0 100 100" class="spinner">
<circle cx="50" cy="50" r="40" />
</svg>
<pl-app></pl-app>
</body>
</html>

View File

@ -1,5 +1,13 @@
import { setPlatform } from "@padloc/core/src/platform";
import { WebPlatform } from "@padloc/app/src/lib/platform";
import "@padloc/app/src/elements/app";
setPlatform(new WebPlatform());
(async () => {
setPlatform(new WebPlatform());
await import("@padloc/app/src/elements/app");
window.onload = () => {
const app = document.createElement("pl-app");
document.body.appendChild(app);
};
})();