Commit 5e3e7faf authored by Yiming Zhou's avatar Yiming Zhou Committed by Commit Bot

Record a user's Chrome autofill profile and Passwords in the Recorder.

Chrome extensions do not have direct access to a user's Chrome profile information, information such as saved passwords and autofill profiles.
However, the Action Recorder Extension may derive a user's saved passwords and autofill profile by observing the password fields, shipping info fields and payment fields that Chrome autofills.

To land this change, I also had to change how the extension detects the Chrome Autofill action. Prior to this change, the extension detects Chrome Autofill using a combination of keydown and onchange event listeners. This strategy can distinguish user typing actions from Chrome autofill actions, but this stategy does not distinguish Chrome autofill actions from Page JavaScript changing input values. In this change, I switched the extension to using the method outlined in https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 to detect Chrome autofill.

Bug: 855284
Change-Id: I22283d5684237084ca2a8b95d9f4723be46b7746
Reviewed-on: https://chromium-review.googlesource.com/1208446
Commit-Queue: Yiming Zhou <uwyiming@google.com>
Reviewed-by: default avatarJared Saul <jsaul@google.com>
Cr-Commit-Position: refs/heads/master@{#589653}
parent 85683b62
...@@ -57,6 +57,10 @@ ...@@ -57,6 +57,10 @@
{ keyPath: 'action_index', autoIncrement: true }); { keyPath: 'action_index', autoIncrement: true });
db.createObjectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS, db.createObjectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS,
{ autoIncrement: true }); { autoIncrement: true });
db.createObjectStore(Indexed_DB_Vars.AUTOFILL_PROFILE,
{ keyPath: 'type' });
db.createObjectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE,
{ keyPath: ['website', 'username'] });
event.target.transaction.oncomplete = (event) => { event.target.transaction.oncomplete = (event) => {
resolve(db); resolve(db);
}; };
...@@ -85,10 +89,13 @@ ...@@ -85,10 +89,13 @@
async function performTransactionOnRecipeIndexedDB(transactionToPerform) { async function performTransactionOnRecipeIndexedDB(transactionToPerform) {
const db = await openRecipeIndexedDB(); const db = await openRecipeIndexedDB();
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ATTRIBUTES, const transaction = db.transaction([
Indexed_DB_Vars.ACTIONS, Indexed_DB_Vars.ATTRIBUTES,
Indexed_DB_Vars.SAVED_ACTION_PARAMS], Indexed_DB_Vars.ACTIONS,
'readwrite'); Indexed_DB_Vars.SAVED_ACTION_PARAMS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readwrite');
transaction.oncomplete = (event) => { transaction.oncomplete = (event) => {
resolve(event); resolve(event);
}; };
...@@ -115,7 +122,7 @@ ...@@ -115,7 +122,7 @@
}); });
} }
async function addActionToRecipe(action, tabId) { async function addActionToRecipe(action, tabId, skipUpdatingUi) {
const db = await openRecipeIndexedDB(); const db = await openRecipeIndexedDB();
const key = await new Promise((resolve, reject) => { const key = await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ACTIONS], const transaction = db.transaction([Indexed_DB_Vars.ACTIONS],
...@@ -145,13 +152,15 @@ ...@@ -145,13 +152,15 @@
db.close(); db.close();
}); });
// Update the recording UI with the new action. if (!skipUpdatingUi) {
const frameId = await getRecorderUiFrameId(); // Update the recording UI with the new action.
action.action_index = key; const frameId = await getRecorderUiFrameId();
await sendMessageToTab( action.action_index = key;
tabId, await sendMessageToTab(
{ type: RecorderUiMsgEnum.ADD_ACTION, action: action}, tabId,
{ frameId: frameId }); { type: RecorderUiMsgEnum.ADD_ACTION, action: action},
{ frameId: frameId });
}
} }
function removeActionFromRecipe(index) { function removeActionFromRecipe(index) {
...@@ -160,13 +169,32 @@ ...@@ -160,13 +169,32 @@
}); });
} }
function insertChromeAutofillProfileEntry(entry) {
return performTransactionOnRecipeIndexedDB((transaction) => {
const autofillProfileStore =
transaction.objectStore(Indexed_DB_Vars.AUTOFILL_PROFILE);
autofillProfileStore.add(entry);
});
}
function insertChromePasswordManagerProfileEntry(entry) {
return performTransactionOnRecipeIndexedDB((transaction) => {
const passwordManagerProfileStore =
transaction.objectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE);
passwordManagerProfileStore.add(entry);
});
}
async function getRecipe() { async function getRecipe() {
const db = await openRecipeIndexedDB(); const db = await openRecipeIndexedDB();
let recipe = {}; let recipe = {};
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ATTRIBUTES, const transaction = db.transaction([
Indexed_DB_Vars.ACTIONS], Indexed_DB_Vars.ATTRIBUTES,
'readonly'); Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readonly');
transaction.oncomplete = (event) => { transaction.oncomplete = (event) => {
resolve(recipe); resolve(recipe);
}; };
...@@ -182,6 +210,30 @@ ...@@ -182,6 +210,30 @@
recipe.startingURL = urlReq.result; recipe.startingURL = urlReq.result;
}; };
const autofillProfileStore =
transaction.objectStore(Indexed_DB_Vars.AUTOFILL_PROFILE);
const autofillProfileReq = autofillProfileStore.getAll();
autofillProfileReq.onsuccess = (event) => {
recipe.autofillProfile =
autofillProfileReq.result ? autofillProfileReq.result : [];
};
const passwordManagerProfileStore =
transaction.objectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE);
const passwordManagerProfileReq = passwordManagerProfileStore.getAll();
passwordManagerProfileReq.onsuccess = (event) => {
recipe.passwordManagerProfiles = [];
// Filter out passwords submitted by the user during the course of
// recording.
if (passwordManagerProfileReq.result) {
for (const entry of passwordManagerProfileReq.result) {
if (entry.submittedByUser === undefined) {
recipe.passwordManagerProfiles.push(entry);
}
}
}
};
const actionsStore = transaction.objectStore(Indexed_DB_Vars.ACTIONS); const actionsStore = transaction.objectStore(Indexed_DB_Vars.ACTIONS);
const actionsReq = actionsStore.getAll(); const actionsReq = actionsStore.getAll();
actionsReq.onsuccess = (event) => { actionsReq.onsuccess = (event) => {
...@@ -223,13 +275,14 @@ ...@@ -223,13 +275,14 @@
}); });
} }
function savePasswordEventParams(userName, password) { function savePasswordEventParams(username, password, website) {
return performTransactionOnRecipeIndexedDB((transaction) => { return performTransactionOnRecipeIndexedDB((transaction) => {
const attributeStore = const attributeStore =
transaction.objectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS); transaction.objectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS);
attributeStore.put({ attributeStore.put({
userName: userName, username: username,
password: password password: password,
website: website
}, Indexed_DB_Vars.PASSWORD_MANAGER_PARAMS); }, Indexed_DB_Vars.PASSWORD_MANAGER_PARAMS);
}); });
} }
...@@ -538,23 +591,47 @@ ...@@ -538,23 +591,47 @@
} }
async function setPasswordEventParams(params, mainFrameIsReady) { async function setPasswordEventParams(params, mainFrameIsReady) {
await savePasswordEventParams(params.userName, params.password); // Add an empty entry to the Password Manager Profile Table.
//
// The Password Manager Profile Table stores the saved passwords a user
// has at the start of recording.
//
// An empty entry is an entry consisting of an origin and a user name,
// but not a password.
//
// The background script adds Password Manager Profile table entries by
// insertion - in other words, the background script cannot override an
// existing entry. The background script creates an empty entry to denote
// that at the start of recording, Chrome did not have a saved password for
// the specified username on the specified origin. If a user saves a new
// password during the course of recording, the empty entry prevents the
// background from erroneously recording the new entry as present at the
// start of recording.
await insertChromePasswordManagerProfileEntry({
submittedByUser: true,
username: params.username,
website: params.website
});
await savePasswordEventParams(params.username, params.password,
params.website);
if (!mainFrameIsReady) { if (!mainFrameIsReady) {
return true; return true;
} }
return await sendPasswordEventParamsToUi(params.userName, params.password); return await sendPasswordEventParamsToUi(params.username, params.password);
} }
async function sendPasswordEventParamsToUi(userName, password) { async function sendPasswordEventParamsToUi(username, password, website) {
const tabId = await getRecordingTabId(); const tabId = await getRecordingTabId();
const frameId = await getRecorderUiFrameId(); const frameId = await getRecorderUiFrameId();
const response = await sendMessageToTab( const response = await sendMessageToTab(
tabId, tabId,
{ type: RecorderUiMsgEnum.SET_PASSWORD_MANAGER_ACTION_PARAMS, { type: RecorderUiMsgEnum.SET_PASSWORD_MANAGER_ACTION_PARAMS,
userName: userName, username: username,
password: password }, password: password,
website: website },
{ frameId: frameId }); { frameId: frameId });
return response; return response;
} }
...@@ -669,7 +746,7 @@ ...@@ -669,7 +746,7 @@
url: details.url, url: details.url,
context: { 'isIframe': false }, context: { 'isIframe': false },
type: ActionTypeEnum.LOAD_PAGE type: ActionTypeEnum.LOAD_PAGE
}); }, tabId, true);
const state = await getRecordingState(); const state = await getRecordingState();
if (state === RecorderStateEnum.HIDDEN) { if (state === RecorderStateEnum.HIDDEN) {
...@@ -759,6 +836,14 @@ ...@@ -759,6 +836,14 @@
// message originates from the main frame. // message originates from the main frame.
sender.frameId != 0); sender.frameId != 0);
return false; return false;
case RecorderMsgEnum.SET_AUTOFILL_PROFILE_ENTRY:
insertChromeAutofillProfileEntry(request.entry);
sendResponse(true);
return false;
case RecorderMsgEnum.SET_PASSWORD_MANAGER_PROFILE_ENTRY:
insertChromePasswordManagerProfileEntry(request.entry);
sendResponse(true);
return false;
default: default:
} }
return false; return false;
......
...@@ -53,7 +53,9 @@ const RecorderMsgEnum = { ...@@ -53,7 +53,9 @@ const RecorderMsgEnum = {
CANCEL: 'cancel-recording', CANCEL: 'cancel-recording',
GET_IFRAME_NAME: 'get-iframe-name', GET_IFRAME_NAME: 'get-iframe-name',
ADD_ACTION: 'record-action', ADD_ACTION: 'record-action',
SET_PASSWORD_MANAGER_ACTION_PARAMS: 'set-password-manager-action-params' SET_PASSWORD_MANAGER_ACTION_PARAMS: 'set-password-manager-action-params',
SET_AUTOFILL_PROFILE_ENTRY: 'set-autofill-profile-entry',
SET_PASSWORD_MANAGER_PROFILE_ENTRY: 'set-password-manager-profile-entry'
}; };
const Local_Storage_Vars = { const Local_Storage_Vars = {
...@@ -74,4 +76,8 @@ const Indexed_DB_Vars = { ...@@ -74,4 +76,8 @@ const Indexed_DB_Vars = {
// actions. // actions.
SAVED_ACTION_PARAMS: 'Saved_Action_Params', SAVED_ACTION_PARAMS: 'Saved_Action_Params',
PASSWORD_MANAGER_PARAMS: 'password_manager_params', PASSWORD_MANAGER_PARAMS: 'password_manager_params',
// The 'Profile' tables stores the user's Chrome autofill profile and Chrome
// password manager profile.
AUTOFILL_PROFILE: 'autofill_profile',
PASSWORD_MANAGER_PROFILE: 'password_manager_profile'
}; };
...@@ -187,10 +187,8 @@ ...@@ -187,10 +187,8 @@
let started = false; let started = false;
let iframeContextMap = {}; let iframeContextMap = {};
function isAutofillableElement(element) { function resetAutofillTriggerElement() {
const autofillPrediction = element.getAttribute('autofill-prediction'); autofillTriggerElementInfo = null;
return (autofillPrediction !== null && autofillPrediction !== '' &&
autofillPrediction !== 'UNKNOWN_TYPE');
} }
function isEditableInputElement(element) { function isEditableInputElement(element) {
...@@ -221,103 +219,169 @@ ...@@ -221,103 +219,169 @@
.indexOf(element.getAttribute('type')) === -1); .indexOf(element.getAttribute('type')) === -1);
} }
/** async function extractAndSendChromePasswordManagerProfile(passwordField) {
* Returns true if |element| is probably a clickable element. // Extract the user name field.
* const form = passwordField.form;
* @param {Element} element The element to be checked. const usernameField = form.querySelector(
* @return {boolean} True if the element is probably clickable. `*[form_signature][pm_parser_annotation='username_element']`);
*/ if (!usernameField) {
function isClickableElementOrInput(element) { console.warn('Failed to detect the user name field!');
return (element.tagName == 'A' || return;
element.tagName == 'BUTTON' || }
element.tagName == 'IMG' ||
element.tagName == 'INPUT' || const autofilledPasswordManagerProfile = {
element.tagName == 'LABEL' || username: usernameField.value,
element.tagName == 'SPAN' || password: passwordField.value,
element.tagName == 'SUBMIT' || website: window.location.origin
element.getAttribute('href')); };
} return await sendRuntimeMessageToBackgroundScript({
type: RecorderMsgEnum.SET_PASSWORD_MANAGER_PROFILE_ENTRY,
function addActionToRecipe(action) { entry: autofilledPasswordManagerProfile
return sendRuntimeMessageToBackgroundScript({ });
}
async function sendChromeAutofillProfileEntry(field) {
const entry = {
type: field.getAttribute('autofill-prediction'),
value: field.value
};
return await sendRuntimeMessageToBackgroundScript({
type: RecorderMsgEnum.SET_AUTOFILL_PROFILE_ENTRY,
entry: entry
});
}
async function addActionToRecipe(action) {
return await sendRuntimeMessageToBackgroundScript({
type: RecorderMsgEnum.ADD_ACTION, type: RecorderMsgEnum.ADD_ACTION,
action: action action: action
}); });
} }
function onInputChangeActionHandler(event) { async function onUserMakingSelectionChange(element) {
const selector = buildXPathForElement(event.target); const selector = buildXPathForElement(element);
const elementReadyState = automation_helper.getElementState(event.target); const elementReadyState = automation_helper.getElementState(element);
const autofillPrediction = const index = element.options.selectedIndex;
event.target.getAttribute('autofill-prediction');
console.log(`Select detected on: ${selector} with '${index}'`);
const action = {
context: frameContext,
index: index,
selector: selector,
type: ActionTypeEnum.SELECT,
visibility: elementReadyState
};
await addActionToRecipe(action);
}
async function onUserFinishingTypingInput(element) {
const selector = buildXPathForElement(element);
const elementReadyState = automation_helper.getElementState(element);
console.log(`Typing detected on: ${selector}`);
// Distinguish between typing inside password input fields and
// other type of text input fields.
//
// This extension generates test recipes to be consumed by the Captured
// Sites Automation Framework. The automation framework replays a typing
// action by using JavaScript to set the value of a text input field.
//
// However, to trigger the Chrome Password Manager, the automation
// framework must simulate user typing inside the password field by
// sending individual character keyboard input - because Chrome Password
// Manager deliberately ignores forms filled by JavaScript.
//
// Simulating keyboard input is a less reliable and therefore the less
// preferred way for filling text inputs. The Automation Framework uses
// keyboard input only when necessary. So this extension separates
// typing password actions from other typing actions.
const isPasswordField = isPasswordInputElement(event.target);
const action = {
context: frameContext,
selector: selector,
type:
isPasswordField ? ActionTypeEnum.TYPE_PASSWORD: ActionTypeEnum.TYPE,
value: element.value,
visibility: elementReadyState
};
await addActionToRecipe(action);
}
async function onUserInvokingAutofill() {
console.log(
`Triggered autofill on ${autofillTriggerElementInfo.selector}`);
const autofillAction = {
selector: autofillTriggerElementInfo.selector,
context: frameContext,
type: ActionTypeEnum.AUTOFILL,
visibility: autofillTriggerElementInfo.visibility
};
resetAutofillTriggerElement();
await addActionToRecipe(autofillAction);
}
async function onChromeAutofillingNonPasswordInput(element) {
const selector = buildXPathForElement(element);
const elementReadyState = automation_helper.getElementState(element);
const value = element.value;
const autofillType = element.getAttribute('autofill-prediction');
console.log(`Autofill detected on: ${selector} with value '${value}'`);
let action = { let action = {
context: frameContext,
expectedValue: value,
selector: selector,
type: ActionTypeEnum.VALIDATE_FIELD,
visibility: elementReadyState
};
if (autofillType) {
action.expectedAutofillType = autofillType;
}
await addActionToRecipe(action);
if (autofillType) {
await sendChromeAutofillProfileEntry(element);
}
}
async function onChromeAutofillingPasswordInput(element) {
const elementReadyState = automation_helper.getElementState(element);
const selector = buildXPathForElement(element);
const value = element.value;
console.log(`Autofill detected on: ${selector} with value '${value}'`);
let validateAction = {
selector: selector, selector: selector,
context: frameContext, context: frameContext,
expectedValue: value,
type: ActionTypeEnum.VALIDATE_FIELD,
visibility: elementReadyState visibility: elementReadyState
}; };
await addActionToRecipe(validateAction);
if (event.target.localName === 'select') { await extractAndSendChromePasswordManagerProfile(element);
if (document.activeElement === event.target) { }
const index = event.target.options.selectedIndex;
console.log(`Select detected on: ${selector} with '${index}'`);
action.type = ActionTypeEnum.SELECT;
action.index = index;
addActionToRecipe(action);
} else {
action.type = ActionTypeEnum.VALIDATE_FIELD;
action.expectedValue = event.target.value;
if (autofillPrediction) {
action.expectedAutofillType = autofillPrediction;
}
addActionToRecipe(action);
}
} else if (lastTypingEventInfo &&
lastTypingEventInfo.target === event.target &&
lastTypingEventInfo.value === event.target.value) {
console.log(`Typing detected on: ${selector}`);
// Distinguish between typing inside password input fields and async function onInputChangeActionHandler(event) {
// other type of text input fields. if (event.target.autofilledByChrome &&
// isChromeRecognizedPasswordField(event.target)) {
// This extension generates test recipes to be consumed by the Captured await onChromeAutofillingPasswordInput(event.target);
// Sites Automation Framework. The automation framework replays a typing } else if (event.target.autofilledByChrome) {
// action by using JavaScript to set the value of a text input field.
// However, to trigger the Chrome Password Manager, the automation
// framework must simulate user typing inside the password field by
// sending individual character keyboard input - because Chrome Password
// Manager deliberately ignores forms filled by JavaScript.
//
// Simulating keyboard input is a less reliable and therefore the less
// preferred way for filling text inputs. The Automation Framework uses
// keyboard input only when necessary. So this extension separates
// typing password actions from other typing actions.
const isPasswordField = isPasswordInputElement(event.target);
action.type =
isPasswordField ?
ActionTypeEnum.TYPE_PASSWORD:
ActionTypeEnum.TYPE;
action.value = event.target.value;
addActionToRecipe(action);
} else {
// If the user has previously clicked on a field that can trigger // If the user has previously clicked on a field that can trigger
// autofill, add a trigger autofill action. // autofill, add a trigger autofill action.
if (autofillTriggerElementInfo !== null) { if (autofillTriggerElementInfo !== null) {
console.log(`Triggered autofill on ${autofillTriggerElementInfo.selector}`); await onUserInvokingAutofill();
let autofillAction = {
selector: autofillTriggerElementInfo.selector,
context: frameContext,
visibility: autofillTriggerElementInfo.visibility
};
autofillAction.type = ActionTypeEnum.AUTOFILL;
addActionToRecipe(autofillAction);
autofillTriggerElementInfo = null;
} }
action.type = ActionTypeEnum.VALIDATE_FIELD; await onChromeAutofillingNonPasswordInput(event.target);
action.expectedValue = event.target.value; } else if (event.target.localName === 'select') {
if (autofillPrediction) { await onUserMakingSelectionChange(event.target);
action.expectedAutofillType = autofillPrediction; } else if (lastTypingEventInfo &&
} lastTypingEventInfo.target === event.target &&
addActionToRecipe(action); lastTypingEventInfo.value === event.target.value) {
await onUserFinishingTypingInput(event.target);
} }
} }
...@@ -326,6 +390,8 @@ ...@@ -326,6 +390,8 @@
inputElements.forEach((element) => { inputElements.forEach((element) => {
if (isEditableInputElement(element)) { if (isEditableInputElement(element)) {
element.addEventListener('change', onInputChangeActionHandler, true); element.addEventListener('change', onInputChangeActionHandler, true);
element.addEventListener('animationstart', onAnimationStartHandler,
true);
} }
}); });
} }
...@@ -335,67 +401,96 @@ ...@@ -335,67 +401,96 @@
inputElements.forEach((element) => { inputElements.forEach((element) => {
if (isEditableInputElement(element)) { if (isEditableInputElement(element)) {
element.removeEventListener('change', onInputChangeActionHandler, true); element.removeEventListener('change', onInputChangeActionHandler, true);
element.removeEventListener('animationstart', onAnimationStartHandler,
true);
} }
}); });
} }
function onClickActionHander(event) { // If a field that can trigger autofill is clicked, save the the element
if (event.button === Buttons.LEFT_BUTTON && // selector path, as the user could have clicked this element to trigger
// Ignore if the event target is the html element. The click event // autofill.
// triggers with the entire html element as the target when the user function onClickingAutofillableElement(element) {
// clicks on a scroll bar. const selector = buildXPathForElement(event.target);
event.target.localName !== 'html') { const elementReadyState = automation_helper.getElementState(event.target);
const element = event.target; autofillTriggerElementInfo = {
const elementReadyState = selector: selector,
automation_helper.getElementState(event.target); visibility: elementReadyState
const selector = buildXPathForElement(element); }
console.log(`Left-click detected on: ${selector}`); }
if (isEditableInputElement(element)) { async function onLeftMouseClickingPageElement(element) {
// If a field that can trigger autofill is clicked, save the // Reset the autofill trigger element.
// the element selector path, as the user could have clicked resetAutofillTriggerElement();
// this element to trigger autofill.
if (isAutofillableElement(element) && canTriggerAutofill(element)) { // Do not record left mouse clicks on editable inputs.
autofillTriggerElementInfo = { // These clicks should always precede either a typing action, or an
selector: selector, // autofill action. The extension will record typing actions and
visibility: elementReadyState // autofill actions separately.
} if (isEditableInputElement(element)) {
} if (canTriggerAutofill(element)) {
} else { onClickingAutofillableElement(element);
addActionToRecipe({
selector: selector,
visibility: elementReadyState,
context: frameContext,
type: ActionTypeEnum.CLICK
});
autofillTriggerElementInfo = null;
} }
} else if (event.button === Buttons.RIGHT_BUTTON) { return;
const element = event.target;
const selector = buildXPathForElement(element);
const elementReadyState =
automation_helper.getElementState(event.target);
console.log(`Right-click detected on: ${selector}`);
addActionToRecipe({
selector: selector,
visibility: elementReadyState,
context: frameContext,
type: ActionTypeEnum.HOVER
});
} }
// Ignore left mouse clicks on the html element. A page fires an event
// with the entire html element as the target when the user clicks on
// Chrome's side scroll bar.
if (event.target.localName === 'html')
return;
const elementReadyState =
automation_helper.getElementState(element);
const selector = buildXPathForElement(element);
console.log(`Left-click detected on: ${selector}`);
await addActionToRecipe({
selector: selector,
visibility: elementReadyState,
context: frameContext,
type: ActionTypeEnum.CLICK
});
} }
function onKeyUpActionHandler(event) { async function onRightMouseClickingPageElement(element) {
if (event.key === 'Enter') { const selector = buildXPathForElement(element);
const elementReadyState = const elementReadyState =
automation_helper.getElementState(event.target); automation_helper.getElementState(element);
const selector = buildXPathForElement(event.target);
addActionToRecipe({ console.log(`Right-click detected on: ${selector}`);
await addActionToRecipe({
selector: selector, selector: selector,
visibility: elementReadyState, visibility: elementReadyState,
context: frameContext, context: frameContext,
type: ActionTypeEnum.PRESS_ENTER type: ActionTypeEnum.HOVER
}); });
}
async function onClickActionHander(event) {
if (event.button === Buttons.LEFT_BUTTON) {
await onLeftMouseClickingPageElement(event.target);
} else if (event.button === Buttons.RIGHT_BUTTON) {
await onRightMouseClickingPageElement(event.target);
}
}
async function onEnterKeyUp(element) {
const elementReadyState = automation_helper.getElementState(element);
const selector = buildXPathForElement(element);
console.log(`Enter detected on: ${selector}'`);
await addActionToRecipe({
selector: selector,
visibility: elementReadyState,
context: frameContext,
type: ActionTypeEnum.PRESS_ENTER
});
}
async function onKeyUpActionHandler(event) {
if (event.key === 'Enter') {
return await onEnterKeyUp(event.target);
} }
if (isEditableInputElement(event.target)) { if (isEditableInputElement(event.target)) {
...@@ -408,13 +503,13 @@ ...@@ -408,13 +503,13 @@
} }
} }
function onPasswordFormSubmitHandler(event) { async function onPasswordFormSubmitHandler(event) {
const form = event.target; const form = event.target;
// Extract the form signature value from the form. // Extract the form signature value from the form.
const fields = form.querySelectorAll( const fields = form.querySelectorAll(
`*[form_signature][pm_parser_annotation]`); `*[form_signature][pm_parser_annotation]`);
let userName = null; let username = null;
let password = null; let password = null;
for (const field of fields) { for (const field of fields) {
const passwordManagerAnnotation = const passwordManagerAnnotation =
...@@ -426,22 +521,25 @@ ...@@ -426,22 +521,25 @@
password = field.value; password = field.value;
break; break;
case 'username_element': case 'username_element':
userName = field.value; username = field.value;
break; break;
default: default:
} }
} }
if (!userName || !password) { if (!username || !password) {
// The form is missing a user name field or a password field. // The form is missing a user name field or a password field.
// The content script should not forward an incomplete password form to // The content script should not forward an incomplete password form to
// the recorder extension. Exit. // the recorder extension. Exit.
return; return;
} }
sendRuntimeMessageToBackgroundScript({ await sendRuntimeMessageToBackgroundScript({
type: RecorderMsgEnum.SET_PASSWORD_MANAGER_ACTION_PARAMS, type: RecorderMsgEnum.SET_PASSWORD_MANAGER_ACTION_PARAMS,
params: { userName: userName, password: password } params: {
username: username,
password: password,
website: window.location.origin }
}); });
} }
...@@ -459,8 +557,48 @@ ...@@ -459,8 +557,48 @@
}); });
} }
function addCssStyleToTriggerAutofillEvents() {
let style = document.createElement("style");
style.type = 'text/css';
const css = `@keyframes onAutoFillStart {
from {/**/} to {/**/}
}
@keyframes onAutoFillCancel {
from {/**/} to {/**/}
}
input:-webkit-autofill,
select:-webkit-autofill,
textarea:-webkit-autofill {
animation-name: onAutoFillStart;
}
input:not(:-webkit-autofill),
select:not(:-webkit-autofill),
textarea:not(:-webkit-autofill) {
animation-name: onAutoFillCancel;
}`;
style.appendChild(document.createTextNode(css));
document.getElementsByTagName('head')[0].appendChild(style);
}
function onAnimationStartHandler(event) {
switch (event.animationName) {
case 'onAutoFillStart':
event.target.autofilledByChrome = true;
break;
case 'onAutoFillCancel':
event.target.autofilledByChrome = false;
break;
default:
}
}
function startRecording(context) { function startRecording(context) {
frameContext = context; frameContext = context;
// Add a css rule to allow the extension to detect when Chrome
// autofills password input fields.
addCssStyleToTriggerAutofillEvents();
// Register on change listeners on all the input elements. // Register on change listeners on all the input elements.
registerOnInputChangeActionListener(document); registerOnInputChangeActionListener(document);
registerOnPasswordFormSubmitHandler(document); registerOnPasswordFormSubmitHandler(document);
......
...@@ -127,9 +127,14 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -127,9 +127,14 @@ document.addEventListener('DOMContentLoaded', () => {
break; break;
case ActionTypeEnum.VALIDATE_FIELD: case ActionTypeEnum.VALIDATE_FIELD:
actionLabel.textContent = 'validate field'; actionLabel.textContent = 'validate field';
actionDetailLabel.textContent = `check that field if (action.expectedAutofillType) {
(${action.expectedAutofillType}) has the value actionDetailLabel.textContent = `check that field
'${action.expectedValue}'`; (${action.expectedAutofillType}) has the value
'${action.expectedValue}'`;
} else {
actionDetailLabel.textContent = `check that field
has the value '${action.expectedValue}'`;
}
break; break;
case ActionTypeEnum.SAVE_PASSWORD: case ActionTypeEnum.SAVE_PASSWORD:
actionLabel.textContent = 'save password'; actionLabel.textContent = 'save password';
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment