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,9 +89,12 @@ ...@@ -85,9 +89,12 @@
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.ATTRIBUTES,
Indexed_DB_Vars.ACTIONS, Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.SAVED_ACTION_PARAMS], Indexed_DB_Vars.SAVED_ACTION_PARAMS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readwrite'); '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,6 +152,7 @@ ...@@ -145,6 +152,7 @@
db.close(); db.close();
}); });
if (!skipUpdatingUi) {
// Update the recording UI with the new action. // Update the recording UI with the new action.
const frameId = await getRecorderUiFrameId(); const frameId = await getRecorderUiFrameId();
action.action_index = key; action.action_index = key;
...@@ -153,6 +161,7 @@ ...@@ -153,6 +161,7 @@
{ type: RecorderUiMsgEnum.ADD_ACTION, action: action}, { type: RecorderUiMsgEnum.ADD_ACTION, action: action},
{ frameId: frameId }); { frameId: frameId });
} }
}
function removeActionFromRecipe(index) { function removeActionFromRecipe(index) {
return performTransactionOnRecipeIndexedDB((transaction) => { return performTransactionOnRecipeIndexedDB((transaction) => {
...@@ -160,12 +169,31 @@ ...@@ -160,12 +169,31 @@
}); });
} }
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,
Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readonly'); '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,67 +219,73 @@ ...@@ -221,67 +219,73 @@
.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');
let action = { console.log(`Select detected on: ${selector} with '${index}'`);
selector: selector, const action = {
context: frameContext, context: frameContext,
index: index,
selector: selector,
type: ActionTypeEnum.SELECT,
visibility: elementReadyState visibility: elementReadyState
}; };
await addActionToRecipe(action);
if (event.target.localName === 'select') {
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}`);
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 // Distinguish between typing inside password input fields and
// other type of text input fields. // other type of text input fields.
// //
// This extension generates test recipes to be consumed by the Captured // This extension generates test recipes to be consumed by the Captured
// Sites Automation Framework. The automation framework replays a typing // Sites Automation Framework. The automation framework replays a typing
// action by using JavaScript to set the value of a text input field. // action by using JavaScript to set the value of a text input field.
//
// However, to trigger the Chrome Password Manager, the automation // However, to trigger the Chrome Password Manager, the automation
// framework must simulate user typing inside the password field by // framework must simulate user typing inside the password field by
// sending individual character keyboard input - because Chrome Password // sending individual character keyboard input - because Chrome Password
...@@ -292,32 +296,92 @@ ...@@ -292,32 +296,92 @@
// keyboard input only when necessary. So this extension separates // keyboard input only when necessary. So this extension separates
// typing password actions from other typing actions. // typing password actions from other typing actions.
const isPasswordField = isPasswordInputElement(event.target); const isPasswordField = isPasswordInputElement(event.target);
action.type =
isPasswordField ? const action = {
ActionTypeEnum.TYPE_PASSWORD: context: frameContext,
ActionTypeEnum.TYPE; selector: selector,
action.value = event.target.value; type:
addActionToRecipe(action); isPasswordField ? ActionTypeEnum.TYPE_PASSWORD: ActionTypeEnum.TYPE,
} else { value: element.value,
// If the user has previously clicked on a field that can trigger visibility: elementReadyState
// autofill, add a trigger autofill action. };
if (autofillTriggerElementInfo !== null) {
console.log(`Triggered autofill on ${autofillTriggerElementInfo.selector}`); await addActionToRecipe(action);
let autofillAction = { }
async function onUserInvokingAutofill() {
console.log(
`Triggered autofill on ${autofillTriggerElementInfo.selector}`);
const autofillAction = {
selector: autofillTriggerElementInfo.selector, selector: autofillTriggerElementInfo.selector,
context: frameContext, context: frameContext,
type: ActionTypeEnum.AUTOFILL,
visibility: autofillTriggerElementInfo.visibility visibility: autofillTriggerElementInfo.visibility
}; };
autofillAction.type = ActionTypeEnum.AUTOFILL; resetAutofillTriggerElement();
addActionToRecipe(autofillAction); await addActionToRecipe(autofillAction);
autofillTriggerElementInfo = null;
} }
action.type = ActionTypeEnum.VALIDATE_FIELD;
action.expectedValue = event.target.value; async function onChromeAutofillingNonPasswordInput(element) {
if (autofillPrediction) { const selector = buildXPathForElement(element);
action.expectedAutofillType = autofillPrediction; 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 = {
context: frameContext,
expectedValue: value,
selector: selector,
type: ActionTypeEnum.VALIDATE_FIELD,
visibility: elementReadyState
};
if (autofillType) {
action.expectedAutofillType = autofillType;
} }
addActionToRecipe(action); 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,
context: frameContext,
expectedValue: value,
type: ActionTypeEnum.VALIDATE_FIELD,
visibility: elementReadyState
};
await addActionToRecipe(validateAction);
await extractAndSendChromePasswordManagerProfile(element);
}
async function onInputChangeActionHandler(event) {
if (event.target.autofilledByChrome &&
isChromeRecognizedPasswordField(event.target)) {
await onChromeAutofillingPasswordInput(event.target);
} else if (event.target.autofilledByChrome) {
// If the user has previously clicked on a field that can trigger
// autofill, add a trigger autofill action.
if (autofillTriggerElementInfo !== null) {
await onUserInvokingAutofill();
}
await onChromeAutofillingNonPasswordInput(event.target);
} else if (event.target.localName === 'select') {
await onUserMakingSelectionChange(event.target);
} else if (lastTypingEventInfo &&
lastTypingEventInfo.target === event.target &&
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,62 +401,86 @@ ...@@ -335,62 +401,86 @@
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;
const elementReadyState =
automation_helper.getElementState(event.target);
const selector = buildXPathForElement(element);
console.log(`Left-click detected on: ${selector}`);
if (isEditableInputElement(element)) {
// If a field that can trigger autofill is clicked, save the
// the element selector path, as the user could have clicked
// this element to trigger autofill.
if (isAutofillableElement(element) && canTriggerAutofill(element)) {
autofillTriggerElementInfo = { autofillTriggerElementInfo = {
selector: selector, selector: selector,
visibility: elementReadyState visibility: elementReadyState
} }
} }
} else {
addActionToRecipe({ async function onLeftMouseClickingPageElement(element) {
// Reset the autofill trigger element.
resetAutofillTriggerElement();
// Do not record left mouse clicks on editable inputs.
// These clicks should always precede either a typing action, or an
// autofill action. The extension will record typing actions and
// autofill actions separately.
if (isEditableInputElement(element)) {
if (canTriggerAutofill(element)) {
onClickingAutofillableElement(element);
}
return;
}
// 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, selector: selector,
visibility: elementReadyState, visibility: elementReadyState,
context: frameContext, context: frameContext,
type: ActionTypeEnum.CLICK type: ActionTypeEnum.CLICK
}); });
autofillTriggerElementInfo = null;
} }
} else if (event.button === Buttons.RIGHT_BUTTON) {
const element = event.target; async function onRightMouseClickingPageElement(element) {
const selector = buildXPathForElement(element); const selector = buildXPathForElement(element);
const elementReadyState = const elementReadyState =
automation_helper.getElementState(event.target); automation_helper.getElementState(element);
console.log(`Right-click detected on: ${selector}`); console.log(`Right-click detected on: ${selector}`);
addActionToRecipe({ await addActionToRecipe({
selector: selector, selector: selector,
visibility: elementReadyState, visibility: elementReadyState,
context: frameContext, context: frameContext,
type: ActionTypeEnum.HOVER 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);
}
} }
function onKeyUpActionHandler(event) { async function onEnterKeyUp(element) {
if (event.key === 'Enter') { const elementReadyState = automation_helper.getElementState(element);
const elementReadyState = const selector = buildXPathForElement(element);
automation_helper.getElementState(event.target);
const selector = buildXPathForElement(event.target); console.log(`Enter detected on: ${selector}'`);
addActionToRecipe({ await addActionToRecipe({
selector: selector, selector: selector,
visibility: elementReadyState, visibility: elementReadyState,
context: frameContext, context: frameContext,
...@@ -398,6 +488,11 @@ ...@@ -398,6 +488,11 @@
}); });
} }
async function onKeyUpActionHandler(event) {
if (event.key === 'Enter') {
return await onEnterKeyUp(event.target);
}
if (isEditableInputElement(event.target)) { if (isEditableInputElement(event.target)) {
lastTypingEventInfo = { lastTypingEventInfo = {
target: event.target, target: 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';
if (action.expectedAutofillType) {
actionDetailLabel.textContent = `check that field actionDetailLabel.textContent = `check that field
(${action.expectedAutofillType}) has the value (${action.expectedAutofillType}) has the value
'${action.expectedValue}'`; '${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