Intelligently fill forms in iframes (#22)
This commit is contained in:
@@ -60,6 +60,105 @@ function copyToClipboard(text) {
|
|||||||
document.execCommand("copy");
|
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
|
* Get Local settings from the extension
|
||||||
*
|
*
|
||||||
@@ -239,29 +338,26 @@ async function handleMessage(settings, message, sendResponse) {
|
|||||||
break;
|
break;
|
||||||
case "fill":
|
case "fill":
|
||||||
try {
|
try {
|
||||||
var tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
|
// get tab info
|
||||||
await chrome.tabs.executeScript(tab.id, { file: "js/inject.dist.js" });
|
var targetTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
|
||||||
// check login fields
|
|
||||||
if (message.login.fields.login === null) {
|
// dispatch initial fill request
|
||||||
throw new Error("No login is available");
|
var filledFields = await fillFields(targetTab, message.login, ["login", "secret"]);
|
||||||
}
|
|
||||||
if (message.login.fields.secret === null) {
|
// no need to check filledFields, because fillFields() already throws an error if empty
|
||||||
throw new Error("No password is available");
|
sendResponse({ status: "ok", filledFields: filledFields });
|
||||||
}
|
|
||||||
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" });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendResponse({
|
try {
|
||||||
status: "error",
|
sendResponse({
|
||||||
message: "Unable to fill credentials: " + e.toString()
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -335,7 +431,10 @@ async function parseFields(settings, login) {
|
|||||||
|
|
||||||
// assign to fields
|
// assign to fields
|
||||||
for (var key in login.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];
|
login.fields[key] = parts[1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -73,24 +73,60 @@
|
|||||||
*
|
*
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*
|
*
|
||||||
* @param object login Login fields
|
* @param object request Form fill request
|
||||||
* @param bool autoSubmit Whether to autosubmit the login form
|
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function fillLogin(login, autoSubmit = false) {
|
function fillLogin(request) {
|
||||||
|
var autoSubmit = false;
|
||||||
|
var filledFields = [];
|
||||||
|
|
||||||
|
// get the login form
|
||||||
var loginForm = form();
|
var loginForm = form();
|
||||||
|
|
||||||
update(USERNAME_FIELDS, login.login, loginForm);
|
// don't attempt to fill non-secret forms unless non-secret filling is allowed
|
||||||
update(PASSWORD_FIELDS, login.secret, loginForm);
|
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);
|
var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, loginForm);
|
||||||
if (password_inputs.length > 1) {
|
if (password_inputs.length > 1) {
|
||||||
// There is likely a field asking for OTP code, so do not submit form just yet
|
// There is likely a field asking for OTP code, so do not submit form just yet
|
||||||
password_inputs[1].select();
|
password_inputs[1].select();
|
||||||
} else {
|
} else {
|
||||||
window.requestAnimationFrame(function() {
|
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);
|
var submit = find(SUBMIT_FIELDS, loginForm);
|
||||||
|
|
||||||
|
// Try to submit the form, or focus on the submit button (based on user settings)
|
||||||
if (submit) {
|
if (submit) {
|
||||||
if (autoSubmit) {
|
if (autoSubmit) {
|
||||||
submit.click();
|
submit.click();
|
||||||
@@ -115,6 +151,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finished filling things successfully
|
||||||
|
return filledFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -25,9 +25,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"nativeMessaging",
|
"nativeMessaging",
|
||||||
"notifications"
|
"notifications",
|
||||||
],
|
|
||||||
"optional_permissions": [
|
|
||||||
"webRequest",
|
"webRequest",
|
||||||
"webRequestBlocking",
|
"webRequestBlocking",
|
||||||
"http://*/*",
|
"http://*/*",
|
||||||
|
@@ -178,13 +178,6 @@ async function withLogin(action) {
|
|||||||
handleError("Filling login details...", "notice");
|
handleError("Filling login details...", "notice");
|
||||||
break;
|
break;
|
||||||
case "launch":
|
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");
|
handleError("Launching URL...", "notice");
|
||||||
break;
|
break;
|
||||||
case "copyPassword":
|
case "copyPassword":
|
||||||
@@ -207,6 +200,7 @@ async function withLogin(action) {
|
|||||||
throw new Error(response.message);
|
throw new Error(response.message);
|
||||||
} else {
|
} else {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
// fall through to update recent
|
||||||
case "fill":
|
case "fill":
|
||||||
case "copyPassword":
|
case "copyPassword":
|
||||||
case "copyUsername":
|
case "copyUsername":
|
||||||
|
Reference in New Issue
Block a user