NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Crédit Agricole login autofill
// @namespace memmie.lenglet.name
// @author mems <memmie@lenglet.name>
// @homepageURL https://github.com/mems/ca-login-autofill/
// @description Enable password autofill on Crédit Agricole.
// @match https://*.credit-agricole.fr/stb/entreeBam*
// @match https://www.credit-agricole.fr/*/*/acceder-a-mes-comptes.html*
// @match https://*.credit-agricole.fr/ephi/*
// @license MIT
// @updateURL https://openuserjs.org/meta/mems/Cr%C3%A9dit_Agricole_login_autofill.meta.js
// @downloadURL https://openuserjs.org/src/scripts/mems/Cr%C3%A9dit_Agricole_login_autofill.user.js
// @version 1.2.1
// @grant none
// ==/UserScript==
// Fake password input that let password manager fill it and trigger pad
const passwordInput = document.createElement("input");
passwordInput.type = "password";
passwordInput.autocomplete = "current-password";
// Bitwarden is quite strict with form field visibility
// See https://github.com/bitwarden/clients/blob/d444143a651dc93172de8bbee3e88f4c2a22e82d/apps/browser/src/autofill/services/dom-element-visibility.service.ts#L16-L26
//passwordInput.style = "position: fixed; right: 100%; bottom: 100%; opacity: 0;";
passwordInput.style = "position: fixed; z-index:1; opacity: 0.1;";
let element;
// espace projet immobilier (www.*-esimulca-enligne.credit-agricole.fr)
if((element = document.querySelector("script#ui-field-gridpassword-templateBAM[type='text/html']")) && element.textContent.includes("onClickChar(")){
// DOM generated by Angular doesn't exist yet, wait until the required element exist in the DOM
new MutationObserver((mutations, observer) => {
if(!(element = document.querySelector("input[name=PASSWORD][data-bind*=oObfuscatedPassword]"))){
return;
}
observer.disconnect();
const removeLastCharButton = document.querySelector("button[data-bind*='onFixPassword(']");
// Create a map from button to index
const keymap = [...document.querySelectorAll("button[data-bind*='onClickChar(']")].reduce((map, button) => {
const val = button.textContent.trim();
return val !== "" ? map.set(val, button) : map;// there is holes in the keypad
}, new Map());
passwordInput.addEventListener("change", event => {
// Clear current value
while(element.value.length > 0){
removeLastCharButton.dispatchEvent(new MouseEvent("click"));
}
for(const char of passwordInput.value){
if(!keymap.has(char)){
continue;
}
keymap.get(char).dispatchEvent(new MouseEvent("click"));
}
});
element.before(passwordInput);
}).observe(document.body, {childList: true, subtree: true});
}
// comptes en ligne 2020 version
else if((element = document.getElementById("Login-account"))){
const keypad = document.getElementById("clavier_num");
const loginButton = document.querySelector(".Login-button");
let dirty = false;
let clearPasswordButton;
const clearPasswordClickHandler = ({isTrusted}) => {
// Not user action
if(!isTrusted){
return;
}
passwordInput.value = "";
};
const update = () => {
if(keypad.style.display === "none" || !dirty){
return false;
}
dirty = false;
clearPasswordButton.dispatchEvent(new MouseEvent("click"));
// Create a map from button to index
const keymap = [...keypad.querySelectorAll(".Login-key div")].reduce((map, div) => map.set(div.textContent.trim(), div.parentElement), new Map());
for(const char of passwordInput.value){
if(!keymap.has(char)){
continue;
}
keymap.get(char).dispatchEvent(new MouseEvent("click"));
}
return true;
};
passwordInput.addEventListener("change", event => {
dirty = true;
if(!update()){
loginButton.dispatchEvent(new MouseEvent("click"));// async load keypad (other rest of inputs are changed synchronously)
}
});
new MutationObserver(mutations => {
// if keypad is loaded and password is not entered yet
if(mutations.find(({attributeName}) => attributeName === "style")){
clearPasswordButton = document.querySelector("#Login-password ~ .add-clear-x");// only exist after keypad been loaded
clearPasswordButton.addEventListener("click", clearPasswordClickHandler);
update();
}
}).observe(keypad, {
attributes: true,
attributeFilter: ["style"],
// attributeOldValue: true,
});
element.after(passwordInput);
}
// comptes en ligne pre-2020 version
else if((element = document.querySelector("input[name=CCPTE]"))){
// Create a map from button to index
const keymap = [...document.querySelectorAll("#pave-saisie-code a")].reduce((map, anchor) => {
const val = anchor.textContent.trim();
return val !== "" ? map.set(val, anchor.parentElement) : map;// there is holes in the keypad
}, new Map());
const removeLastCharButton = [...document.getElementsByTagName("a")].find(anchor => anchor.textContent.trim() == "Corriger");
const removeLastChar = (function(){with(this){eval(removeLastCharButton.href)}}).bind(unsafeWindow);// use page context, not sandbow context
passwordInput.addEventListener("input", function(event){
// Clear current value
while(removeLastCharButton && document.formulaire.CCCRYC2.value.length > 0){
// removeLastCharButton.dispatchEvent(new MouseEvent("click"));
// but because href="javascript:..." is executed async, this cause an infinite loop here
try{
removeLastChar();
}
catch(error){}
}
for(const char of this.value){
if(!keymap.has(char)){
continue;
}
keymap.get(char).dispatchEvent(new MouseEvent("click"));
}
});
element.after(passwordInput);
// Allow to remove the last char
removeLastCharButton.addEventListener("click", event => {
passwordInput.value = passwordInput.value.slice(0, -1);
});
}