forked from mirrors/gecko-dev
Differential Revision: https://phabricator.services.mozilla.com/D39066 --HG-- extra : moz-landing-system : lando
628 lines
18 KiB
JavaScript
628 lines
18 KiB
JavaScript
/* Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
ChromeUtils.import(
|
|
"resource://services-common/utils.js"
|
|
); /* global: CommonUtils */
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { Accounts } = ChromeUtils.import("resource://gre/modules/Accounts.jsm");
|
|
|
|
XPCOMUtils.defineLazyGetter(
|
|
window,
|
|
"gChromeWin",
|
|
() => window.docShell.rootTreeItem.domWindow
|
|
);
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"EventDispatcher",
|
|
"resource://gre/modules/Messaging.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Snackbars",
|
|
"resource://gre/modules/Snackbars.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"Prompt",
|
|
"resource://gre/modules/Prompt.jsm"
|
|
);
|
|
|
|
var debug = ChromeUtils.import(
|
|
"resource://gre/modules/AndroidLog.jsm",
|
|
{}
|
|
).AndroidLog.d.bind(null, "AboutLogins");
|
|
|
|
var gStringBundle = Services.strings.createBundle(
|
|
"chrome://browser/locale/aboutLogins.properties"
|
|
);
|
|
|
|
function copyStringShowSnackbar(string, notifyString) {
|
|
try {
|
|
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
|
|
Ci.nsIClipboardHelper
|
|
);
|
|
clipboard.copyString(string);
|
|
Snackbars.show(notifyString, Snackbars.LENGTH_LONG);
|
|
} catch (e) {
|
|
debug("Error copying from about:logins");
|
|
Snackbars.show(
|
|
gStringBundle.GetStringFromName("loginsDetails.copyFailed"),
|
|
Snackbars.LENGTH_LONG
|
|
);
|
|
}
|
|
}
|
|
|
|
// Delay filtering while typing in MS
|
|
const FILTER_DELAY = 500;
|
|
|
|
var Logins = {
|
|
_logins: [],
|
|
_filterTimer: null,
|
|
_selectedLogin: null,
|
|
|
|
// Load the logins list, displaying interstitial UI (see
|
|
// #logins-list-loading-body) while loading. There are careful
|
|
// jank-avoiding measures taken in this function; be careful when
|
|
// modifying it!
|
|
//
|
|
// Returns a Promise that resolves to the list of logins, ordered by
|
|
// origin.
|
|
_promiseLogins: function() {
|
|
let contentBody = document.getElementById("content-body");
|
|
let emptyBody = document.getElementById("empty-body");
|
|
let filterIcon = document.getElementById("filter-button");
|
|
|
|
let showSpinner = () => {
|
|
this._toggleListBody(true);
|
|
emptyBody.classList.add("hidden");
|
|
};
|
|
|
|
let getAllLogins = () => {
|
|
let logins = [];
|
|
try {
|
|
logins = Services.logins.getAllLogins();
|
|
} catch (e) {
|
|
// It's likely that the Master Password was not entered; give
|
|
// a hint to the next person.
|
|
throw new Error(
|
|
"Possible Master Password permissions error: " + e.toString()
|
|
);
|
|
}
|
|
|
|
logins.sort((a, b) => a.origin.localeCompare(b.origin));
|
|
|
|
return logins;
|
|
};
|
|
|
|
let hideSpinner = logins => {
|
|
this._toggleListBody(false);
|
|
|
|
if (!logins.length) {
|
|
contentBody.classList.add("hidden");
|
|
filterIcon.classList.add("hidden");
|
|
emptyBody.classList.remove("hidden");
|
|
} else {
|
|
contentBody.classList.remove("hidden");
|
|
emptyBody.classList.add("hidden");
|
|
}
|
|
|
|
return logins;
|
|
};
|
|
|
|
// Return a promise that is resolved after a paint.
|
|
let waitForPaint = () => {
|
|
// We're changing 'display'. We need to wait for the new value to take
|
|
// effect; otherwise, we'll block and never paint a change. Since
|
|
// requestAnimationFrame callback is generally triggered *before* any
|
|
// style flush and layout, we wait for two animation frames. This
|
|
// approach was cribbed from
|
|
// https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469.
|
|
return new Promise(function(resolve, reject) {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
// getAllLogins janks the main-thread. We need to paint before that jank;
|
|
// by throwing the janky load onto the next tick, we paint the spinner; the
|
|
// spinner is CSS animated off-main-thread.
|
|
return Promise.resolve()
|
|
.then(showSpinner)
|
|
.then(waitForPaint)
|
|
.then(getAllLogins)
|
|
.then(hideSpinner);
|
|
},
|
|
|
|
// Reload the logins list, displaying interstitial UI while loading.
|
|
// Update the stored and displayed list upon completion.
|
|
_reloadList: function() {
|
|
this._promiseLogins()
|
|
.then(logins => {
|
|
this._logins = logins;
|
|
this._loadList(logins);
|
|
})
|
|
.catch(e => {
|
|
// There's no way to recover from errors, sadly. Log and make
|
|
// it obvious that something is up.
|
|
this._logins = [];
|
|
debug("Failed to _reloadList!");
|
|
Cu.reportError(e);
|
|
});
|
|
},
|
|
|
|
_toggleListBody: function(isLoading) {
|
|
let contentBody = document.getElementById("content-body");
|
|
let loadingBody = document.getElementById("logins-list-loading-body");
|
|
|
|
if (isLoading) {
|
|
contentBody.classList.add("hidden");
|
|
loadingBody.classList.remove("hidden");
|
|
} else {
|
|
loadingBody.classList.add("hidden");
|
|
contentBody.classList.remove("hidden");
|
|
}
|
|
},
|
|
|
|
init: function() {
|
|
window.addEventListener("popstate", this);
|
|
|
|
Services.obs.addObserver(this, "passwordmgr-storage-changed");
|
|
document
|
|
.getElementById("update-btn")
|
|
.addEventListener("click", this._onSaveEditLogin.bind(this));
|
|
document
|
|
.getElementById("password-btn")
|
|
.addEventListener("click", this._onPasswordBtn.bind(this));
|
|
|
|
let filterInput = document.getElementById("filter-input");
|
|
let filterContainer = document.getElementById("filter-input-container");
|
|
|
|
filterInput.addEventListener("input", event => {
|
|
// Stop any in-progress filter timer
|
|
if (this._filterTimer) {
|
|
clearTimeout(this._filterTimer);
|
|
this._filterTimer = null;
|
|
}
|
|
|
|
// Start a new timer
|
|
this._filterTimer = setTimeout(() => {
|
|
this._filter(event);
|
|
}, FILTER_DELAY);
|
|
});
|
|
|
|
filterInput.addEventListener("blur", event => {
|
|
filterContainer.setAttribute("hidden", true);
|
|
});
|
|
|
|
document
|
|
.getElementById("filter-button")
|
|
.addEventListener("click", event => {
|
|
filterContainer.removeAttribute("hidden");
|
|
filterInput.focus();
|
|
});
|
|
|
|
document.getElementById("filter-clear").addEventListener("click", event => {
|
|
// Stop any in-progress filter timer
|
|
if (this._filterTimer) {
|
|
clearTimeout(this._filterTimer);
|
|
this._filterTimer = null;
|
|
}
|
|
|
|
filterInput.blur();
|
|
filterInput.value = "";
|
|
this._loadList(this._logins);
|
|
});
|
|
|
|
this._showList();
|
|
|
|
this._updatePasswordBtn(true);
|
|
|
|
this._reloadList();
|
|
},
|
|
|
|
uninit: function() {
|
|
Services.obs.removeObserver(this, "passwordmgr-storage-changed");
|
|
window.removeEventListener("popstate", this);
|
|
},
|
|
|
|
_loadList: function(logins) {
|
|
let list = document.getElementById("logins-list");
|
|
let newList = list.cloneNode(false);
|
|
|
|
logins.forEach(login => {
|
|
let item = this._createItemForLogin(login);
|
|
newList.appendChild(item);
|
|
});
|
|
|
|
list.parentNode.replaceChild(newList, list);
|
|
},
|
|
|
|
_showList: function() {
|
|
let loginsListPage = document.getElementById("logins-list-page");
|
|
loginsListPage.classList.remove("hidden");
|
|
|
|
let editLoginPage = document.getElementById("edit-login-page");
|
|
editLoginPage.classList.add("hidden");
|
|
|
|
// If the Show/Hide password button has been flipped, reset it
|
|
if (this._isPasswordBtnInHideMode()) {
|
|
this._updatePasswordBtn(true);
|
|
}
|
|
},
|
|
|
|
_onPopState: function(event) {
|
|
// Called when back/forward is used to change the state of the page
|
|
if (event.state) {
|
|
this._showEditLoginDialog(event.state.id);
|
|
} else {
|
|
this._selectedLogin = null;
|
|
this._showList();
|
|
}
|
|
},
|
|
_showEditLoginDialog: function(login) {
|
|
let listPage = document.getElementById("logins-list-page");
|
|
listPage.classList.add("hidden");
|
|
|
|
let editLoginPage = document.getElementById("edit-login-page");
|
|
editLoginPage.classList.remove("hidden");
|
|
|
|
let usernameField = document.getElementById("username");
|
|
usernameField.value = login.username;
|
|
let passwordField = document.getElementById("password");
|
|
passwordField.value = login.password;
|
|
let domainField = document.getElementById("origin");
|
|
domainField.value = login.origin;
|
|
|
|
let img = document.getElementById("favicon");
|
|
this._loadFavicon(img, login.origin);
|
|
|
|
let headerText = document.getElementById("edit-login-header-text");
|
|
if (login.origin && login.origin != "") {
|
|
headerText.textContent = login.origin;
|
|
} else {
|
|
headerText.textContent = gStringBundle.GetStringFromName(
|
|
"editLogin.fallbackTitle"
|
|
);
|
|
}
|
|
|
|
passwordField.addEventListener("input", event => {
|
|
let newPassword = passwordField.value;
|
|
let updateBtn = document.getElementById("update-btn");
|
|
|
|
if (newPassword === "") {
|
|
updateBtn.disabled = true;
|
|
updateBtn.classList.add("disabled-btn");
|
|
} else if (newPassword !== "" && updateBtn.disabled === true) {
|
|
updateBtn.disabled = false;
|
|
updateBtn.classList.remove("disabled-btn");
|
|
}
|
|
});
|
|
},
|
|
|
|
_onSaveEditLogin: function() {
|
|
let newUsername = document.getElementById("username").value;
|
|
let newPassword = document.getElementById("password").value;
|
|
let origUsername = this._selectedLogin.username;
|
|
let origPassword = this._selectedLogin.password;
|
|
|
|
try {
|
|
if (newUsername === origUsername && newPassword === origPassword) {
|
|
Snackbars.show(
|
|
gStringBundle.GetStringFromName("editLogin.saved1"),
|
|
Snackbars.LENGTH_LONG
|
|
);
|
|
this._showList();
|
|
return;
|
|
}
|
|
|
|
let logins = Services.logins.findLogins(
|
|
this._selectedLogin.origin,
|
|
this._selectedLogin.formActionOrigin,
|
|
this._selectedLogin.httpRealm
|
|
);
|
|
|
|
for (let i = 0; i < logins.length; i++) {
|
|
if (logins[i].username == origUsername) {
|
|
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
|
|
Ci.nsIWritablePropertyBag
|
|
);
|
|
if (newUsername !== origUsername) {
|
|
propBag.setProperty("username", newUsername);
|
|
}
|
|
if (newPassword !== origPassword) {
|
|
propBag.setProperty("password", newPassword);
|
|
}
|
|
// Sync relies on timePasswordChanged to decide whether
|
|
// or not to sync a login, so touch it.
|
|
propBag.setProperty("timePasswordChanged", Date.now());
|
|
Services.logins.modifyLogin(logins[i], propBag);
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Snackbars.show(
|
|
gStringBundle.GetStringFromName("editLogin.couldNotSave"),
|
|
Snackbars.LENGTH_LONG
|
|
);
|
|
return;
|
|
}
|
|
Snackbars.show(
|
|
gStringBundle.GetStringFromName("editLogin.saved1"),
|
|
Snackbars.LENGTH_LONG
|
|
);
|
|
this._showList();
|
|
},
|
|
|
|
_onPasswordBtn: function() {
|
|
this._updatePasswordBtn(this._isPasswordBtnInHideMode());
|
|
},
|
|
|
|
_updatePasswordBtn: function(aShouldShow) {
|
|
let passwordField = document.getElementById("password");
|
|
let button = document.getElementById("password-btn");
|
|
let show = gStringBundle.GetStringFromName("password-btn.show");
|
|
let hide = gStringBundle.GetStringFromName("password-btn.hide");
|
|
if (aShouldShow) {
|
|
passwordField.type = "password";
|
|
button.textContent = show;
|
|
button.classList.remove("password-btn-hide");
|
|
} else {
|
|
passwordField.type = "text";
|
|
button.textContent = hide;
|
|
button.classList.add("password-btn-hide");
|
|
}
|
|
},
|
|
|
|
_isPasswordBtnInHideMode: function() {
|
|
let button = document.getElementById("password-btn");
|
|
return button.classList.contains("password-btn-hide");
|
|
},
|
|
|
|
_showPassword: function(password) {
|
|
let passwordPrompt = new Prompt({
|
|
window: window,
|
|
message: password,
|
|
buttons: [
|
|
gStringBundle.GetStringFromName("loginsDialog.copy"),
|
|
gStringBundle.GetStringFromName("loginsDialog.cancel"),
|
|
],
|
|
}).show(data => {
|
|
switch (data.button) {
|
|
case 0:
|
|
// Corresponds to "Copy password" button.
|
|
copyStringShowSnackbar(
|
|
password,
|
|
gStringBundle.GetStringFromName("loginsDetails.passwordCopied")
|
|
);
|
|
}
|
|
});
|
|
},
|
|
|
|
_onLoginClick: function(event) {
|
|
let loginItem = event.currentTarget;
|
|
let login = loginItem.login;
|
|
if (!login) {
|
|
debug("No login!");
|
|
return;
|
|
}
|
|
|
|
let prompt = new Prompt({
|
|
window: window,
|
|
});
|
|
let menuItems = [
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.editLogin") },
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.delete") },
|
|
{ label: gStringBundle.GetStringFromName("loginsMenu.deleteAll") },
|
|
];
|
|
|
|
prompt.setSingleChoiceItems(menuItems);
|
|
prompt.show(data => {
|
|
// Switch on indices of buttons, as they were added when creating login item.
|
|
switch (data.button) {
|
|
case 0:
|
|
this._showPassword(login.password);
|
|
break;
|
|
case 1:
|
|
copyStringShowSnackbar(
|
|
login.password,
|
|
gStringBundle.GetStringFromName("loginsDetails.passwordCopied")
|
|
);
|
|
break;
|
|
case 2:
|
|
copyStringShowSnackbar(
|
|
login.username,
|
|
gStringBundle.GetStringFromName("loginsDetails.usernameCopied")
|
|
);
|
|
break;
|
|
case 3:
|
|
this._selectedLogin = login;
|
|
this._showEditLoginDialog(login);
|
|
history.pushState({ id: login.guid }, document.title);
|
|
break;
|
|
case 4:
|
|
Accounts.getFirefoxAccount().then(user => {
|
|
const promptMessage = user
|
|
? gStringBundle.GetStringFromName(
|
|
"loginsDialog.confirmDeleteForFxaUser"
|
|
)
|
|
: gStringBundle.GetStringFromName("loginsDialog.confirmDelete");
|
|
const confirmationMessage = gStringBundle.GetStringFromName(
|
|
"loginsDetails.deleted"
|
|
);
|
|
|
|
this._showConfirmationPrompt(
|
|
promptMessage,
|
|
confirmationMessage,
|
|
() => Services.logins.removeLogin(login)
|
|
);
|
|
});
|
|
break;
|
|
case 5:
|
|
Accounts.getFirefoxAccount().then(user => {
|
|
const promptMessage = user
|
|
? gStringBundle.GetStringFromName(
|
|
"loginsDialog.confirmDeleteAllForFxaUser"
|
|
)
|
|
: gStringBundle.GetStringFromName(
|
|
"loginsDialog.confirmDeleteAll"
|
|
);
|
|
const confirmationMessage = gStringBundle.GetStringFromName(
|
|
"loginsDetails.deletedAll"
|
|
);
|
|
|
|
this._showConfirmationPrompt(
|
|
promptMessage,
|
|
confirmationMessage,
|
|
() => Services.logins.removeAllLogins()
|
|
);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
},
|
|
|
|
_showConfirmationPrompt: function(
|
|
promptMessage,
|
|
confirmationMessage,
|
|
actionToPerform
|
|
) {
|
|
new Prompt({
|
|
window: window,
|
|
message: promptMessage,
|
|
buttons: [
|
|
// Use default, generic values
|
|
gStringBundle.GetStringFromName("loginsDialog.confirm"),
|
|
gStringBundle.GetStringFromName("loginsDialog.cancel"),
|
|
],
|
|
}).show(data => {
|
|
switch (data.button) {
|
|
case 0:
|
|
// Corresponds to "confirm" button.
|
|
|
|
actionToPerform();
|
|
|
|
Snackbars.show(confirmationMessage, Snackbars.LENGTH_LONG);
|
|
}
|
|
});
|
|
},
|
|
|
|
_loadFavicon: function(aImg, aOrigin) {
|
|
// Load favicon from cache.
|
|
EventDispatcher.instance
|
|
.sendRequestForResult({
|
|
type: "Favicon:Request",
|
|
url: aOrigin,
|
|
skipNetwork: true,
|
|
})
|
|
.then(
|
|
function(faviconUrl) {
|
|
aImg.style.backgroundImage = "url('" + faviconUrl + "')";
|
|
aImg.style.visibility = "visible";
|
|
},
|
|
function(data) {
|
|
debug("Favicon cache failure : " + data);
|
|
aImg.style.visibility = "visible";
|
|
}
|
|
);
|
|
},
|
|
|
|
_createItemForLogin: function(login) {
|
|
let loginItem = document.createElement("div");
|
|
|
|
loginItem.setAttribute("loginID", login.guid);
|
|
loginItem.className = "login-item list-item";
|
|
|
|
loginItem.addEventListener("click", this, true);
|
|
loginItem.addEventListener("contextmenu", this, true);
|
|
|
|
// Create item icon.
|
|
let img = document.createElement("div");
|
|
img.className = "icon";
|
|
|
|
this._loadFavicon(img, login.origin);
|
|
loginItem.appendChild(img);
|
|
|
|
// Create item details.
|
|
let inner = document.createElement("div");
|
|
inner.className = "inner";
|
|
|
|
let details = document.createElement("div");
|
|
details.className = "details";
|
|
inner.appendChild(details);
|
|
|
|
let titlePart = document.createElement("div");
|
|
titlePart.className = "origin";
|
|
titlePart.textContent = login.origin;
|
|
details.appendChild(titlePart);
|
|
|
|
let versionPart = document.createElement("div");
|
|
versionPart.textContent = login.httpRealm;
|
|
versionPart.className = "realm";
|
|
details.appendChild(versionPart);
|
|
|
|
let descPart = document.createElement("div");
|
|
descPart.textContent = login.username;
|
|
descPart.className = "username";
|
|
inner.appendChild(descPart);
|
|
|
|
loginItem.appendChild(inner);
|
|
loginItem.login = login;
|
|
return loginItem;
|
|
},
|
|
|
|
handleEvent: function(event) {
|
|
switch (event.type) {
|
|
case "popstate": {
|
|
this._onPopState(event);
|
|
break;
|
|
}
|
|
case "contextmenu":
|
|
case "click": {
|
|
this._onLoginClick(event);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
observe: function(subject, topic, data) {
|
|
switch (topic) {
|
|
case "passwordmgr-storage-changed": {
|
|
this._reloadList();
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
_filter: function(event) {
|
|
let value = event.target.value.toLowerCase();
|
|
let logins = this._logins.filter(login => {
|
|
if (login.origin.toLowerCase().includes(value)) {
|
|
return true;
|
|
}
|
|
if (login.username && login.username.toLowerCase().includes(value)) {
|
|
return true;
|
|
}
|
|
if (login.httpRealm && login.httpRealm.toLowerCase().includes(value)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
this._loadList(logins);
|
|
},
|
|
};
|
|
|
|
window.addEventListener("load", Logins.init.bind(Logins));
|
|
window.addEventListener("unload", Logins.uninit.bind(Logins));
|