Intelligently fill forms in iframes (#22)
This commit is contained in:
@@ -60,6 +60,105 @@ function copyToClipboard(text) {
|
||||
document.execCommand("copy");
|
||||
}
|
||||
|
||||
/**
|
||||
* Call injected form-fill code
|
||||
*
|
||||
* @param object tab Target tab
|
||||
* @param object fillRequest Fill request details
|
||||
* @param boolean allFrames Dispatch to all frames
|
||||
* @param boolean allowForeign Allow foreign-origin iframes
|
||||
* @param boolean allowNoSecret Allow forms that don't contain a password field
|
||||
* @return array list of filled fields
|
||||
*/
|
||||
async function dispatchFill(
|
||||
tab,
|
||||
fillRequest,
|
||||
allFrames = false,
|
||||
allowForeign = false,
|
||||
allowNoSecret = false
|
||||
) {
|
||||
fillRequest = Object.assign({}, fillRequest, {
|
||||
allowForeign: allowForeign,
|
||||
allowNoSecret: allowNoSecret
|
||||
});
|
||||
|
||||
var filledFields = await chrome.tabs.executeScript(tab.id, {
|
||||
allFrames: allFrames,
|
||||
code: `window.browserpass.fillLogin(${JSON.stringify(fillRequest)});`
|
||||
});
|
||||
|
||||
// simplify the list of filled fields
|
||||
filledFields = filledFields
|
||||
.reduce((fields, addFields) => fields.concat(addFields), [])
|
||||
.reduce(function(fields, field) {
|
||||
if (!fields.includes(field)) {
|
||||
fields.push(field);
|
||||
}
|
||||
return fields;
|
||||
}, []);
|
||||
|
||||
return filledFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form fields
|
||||
*
|
||||
* @param object tab Target tab
|
||||
* @param object login Login object
|
||||
* @param array fields List of fields to fill
|
||||
* @return array List of filled fields
|
||||
*/
|
||||
async function fillFields(tab, login, fields) {
|
||||
// check that required fields are present
|
||||
for (var field of fields) {
|
||||
if (login.fields[field] === null) {
|
||||
throw new Error(`Required field is missing: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
// inject script
|
||||
await chrome.tabs.executeScript(tab.id, {
|
||||
allFrames: true,
|
||||
file: "js/inject.dist.js"
|
||||
});
|
||||
|
||||
// build fill request
|
||||
var fillRequest = {
|
||||
origin: new URL(tab.url).origin,
|
||||
login: login,
|
||||
fields: fields
|
||||
};
|
||||
|
||||
// fill form via injected script
|
||||
var filledFields = await dispatchFill(tab, fillRequest);
|
||||
|
||||
// try again using same-origin frames if we couldn't fill a password field
|
||||
if (!filledFields.includes("secret")) {
|
||||
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true));
|
||||
}
|
||||
|
||||
// try again using all available frames if we couldn't fill a password field
|
||||
if (!filledFields.includes("secret")) {
|
||||
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true));
|
||||
}
|
||||
|
||||
// try again using same-origin frames, and don't require a password field
|
||||
if (!filledFields.length) {
|
||||
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, false, true));
|
||||
}
|
||||
|
||||
// try again using all available frames, and don't require a password field
|
||||
if (!filledFields.length) {
|
||||
filledFields = filledFields.concat(await dispatchFill(tab, fillRequest, true, true, true));
|
||||
}
|
||||
|
||||
if (!filledFields.length) {
|
||||
throw new Error(`No fillable forms available for fields: ${fields.join(", ")}`);
|
||||
}
|
||||
|
||||
return filledFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Local settings from the extension
|
||||
*
|
||||
@@ -239,29 +338,26 @@ async function handleMessage(settings, message, sendResponse) {
|
||||
break;
|
||||
case "fill":
|
||||
try {
|
||||
var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
|
||||
await chrome.tabs.executeScript(tab.id, { file: "js/inject.dist.js" });
|
||||
// check login fields
|
||||
if (message.login.fields.login === null) {
|
||||
throw new Error("No login is available");
|
||||
}
|
||||
if (message.login.fields.secret === null) {
|
||||
throw new Error("No password is available");
|
||||
}
|
||||
var fillFields = JSON.stringify({
|
||||
login: message.login.fields.login,
|
||||
secret: message.login.fields.secret
|
||||
});
|
||||
// fill form via injected script
|
||||
await chrome.tabs.executeScript(tab.id, {
|
||||
code: `window.browserpass.fillLogin(${fillFields});`
|
||||
});
|
||||
sendResponse({ status: "ok" });
|
||||
// get tab info
|
||||
var targetTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
|
||||
|
||||
// dispatch initial fill request
|
||||
var filledFields = await fillFields(targetTab, message.login, ["login", "secret"]);
|
||||
|
||||
// no need to check filledFields, because fillFields() already throws an error if empty
|
||||
sendResponse({ status: "ok", filledFields: filledFields });
|
||||
} catch (e) {
|
||||
sendResponse({
|
||||
status: "error",
|
||||
message: "Unable to fill credentials: " + e.toString()
|
||||
});
|
||||
try {
|
||||
sendResponse({
|
||||
status: "error",
|
||||
message: e.toString()
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO An error here is typically a closed message port, due to a popup taking focus
|
||||
// away from the extension menu and the menu closing as a result. Need to investigate
|
||||
// whether triggering the extension menu from the background script is possible.
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -335,7 +431,10 @@ async function parseFields(settings, login) {
|
||||
|
||||
// assign to fields
|
||||
for (var key in login.fields) {
|
||||
if (Array.isArray(login.fields[key]) && login.fields[key].indexOf(parts[0]) >= 0) {
|
||||
if (
|
||||
Array.isArray(login.fields[key]) &&
|
||||
login.fields[key].indexOf(parts[0].toLowerCase()) >= 0
|
||||
) {
|
||||
login.fields[key] = parts[1];
|
||||
break;
|
||||
}
|
||||
|
@@ -73,24 +73,60 @@
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param object login Login fields
|
||||
* @param bool autoSubmit Whether to autosubmit the login form
|
||||
* @param object request Form fill request
|
||||
* @return void
|
||||
*/
|
||||
function fillLogin(login, autoSubmit = false) {
|
||||
function fillLogin(request) {
|
||||
var autoSubmit = false;
|
||||
var filledFields = [];
|
||||
|
||||
// get the login form
|
||||
var loginForm = form();
|
||||
|
||||
update(USERNAME_FIELDS, login.login, loginForm);
|
||||
update(PASSWORD_FIELDS, login.secret, loginForm);
|
||||
// don't attempt to fill non-secret forms unless non-secret filling is allowed
|
||||
if (!find(PASSWORD_FIELDS, loginForm) && !request.allowNoSecret) {
|
||||
return filledFields;
|
||||
}
|
||||
|
||||
// ensure the origin is the same, or ask the user for permissions to continue
|
||||
if (window.location.origin !== request.origin) {
|
||||
var message =
|
||||
"You have requested to fill login credentials into an embedded document from a " +
|
||||
"different origin than the main document in this tab. Do you wish to proceed?\n\n" +
|
||||
`Tab origin: ${request.origin}\n` +
|
||||
`Embedded origin: ${window.location.origin}`;
|
||||
if (!request.allowForeign || !confirm(message)) {
|
||||
return filledFields;
|
||||
}
|
||||
}
|
||||
|
||||
// fill login field
|
||||
if (
|
||||
request.fields.includes("login") &&
|
||||
update(USERNAME_FIELDS, request.login.fields.login, loginForm)
|
||||
) {
|
||||
filledFields.push("login");
|
||||
}
|
||||
|
||||
// fill secret field
|
||||
if (
|
||||
request.fields.includes("secret") &&
|
||||
update(PASSWORD_FIELDS, request.login.fields.secret, loginForm)
|
||||
) {
|
||||
filledFields.push("secret");
|
||||
}
|
||||
|
||||
// check for multiple password fields in the login form
|
||||
var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm);
|
||||
if (password_inputs.length > 1) {
|
||||
// There is likely a field asking for OTP code, so do not submit form just yet
|
||||
password_inputs[1].select();
|
||||
} else {
|
||||
window.requestAnimationFrame(function() {
|
||||
// Try to submit the form, or focus on the submit button (based on user settings)
|
||||
// try to locate the submit button
|
||||
var submit = find(SUBMIT_FIELDS, loginForm);
|
||||
|
||||
// Try to submit the form, or focus on the submit button (based on user settings)
|
||||
if (submit) {
|
||||
if (autoSubmit) {
|
||||
submit.click();
|
||||
@@ -115,6 +151,9 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// finished filling things successfully
|
||||
return filledFields;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -25,9 +25,7 @@
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"nativeMessaging",
|
||||
"notifications"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"notifications",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"http://*/*",
|
||||
|
@@ -178,13 +178,6 @@ async function withLogin(action) {
|
||||
handleError("Filling login details...", "notice");
|
||||
break;
|
||||
case "launch":
|
||||
var havePermission = await chrome.permissions.request({
|
||||
permissions: ["webRequest", "webRequestBlocking"],
|
||||
origins: ["http://*/*", "https://*/*"]
|
||||
});
|
||||
if (!havePermission) {
|
||||
throw new Error("Browserpass requires additional permissions to proceed");
|
||||
}
|
||||
handleError("Launching URL...", "notice");
|
||||
break;
|
||||
case "copyPassword":
|
||||
@@ -207,6 +200,7 @@ async function withLogin(action) {
|
||||
throw new Error(response.message);
|
||||
} else {
|
||||
switch (action) {
|
||||
// fall through to update recent
|
||||
case "fill":
|
||||
case "copyPassword":
|
||||
case "copyUsername":
|
||||
|
Reference in New Issue
Block a user