Commit 6f75f8a3 authored by Jared Wein's avatar Jared Wein
Browse files

Bug 1548463 - Base page for HTML-based login manager. r=MattN,flod

Differential Revision: https://phabricator.services.mozilla.com/D29624

--HG--
extra : moz-landing-system : lando
parent afc359bd
......@@ -1747,6 +1747,7 @@ pref("extensions.pocket.site", "getpocket.com");
pref("signon.schemeUpgrades", true);
pref("signon.privateBrowsingCapture.enabled", true);
pref("signon.showAutoCompleteFooter", true);
pref("signon.management.page.enabled", false);
// Enable the "Simplify Page" feature in Print Preview. This feature
// is disabled by default in toolkit.
......
......@@ -30,6 +30,23 @@ let ACTORS = {
};
let LEGACY_ACTORS = {
AboutLogins: {
child: {
matches: ["about:logins"],
module: "resource:///actors/AboutLoginsChild.jsm",
events: {
"AboutLoginsDeleteLogin": {wantUntrusted: true},
"AboutLoginsInit": {wantUntrusted: true},
},
messages: [
"AboutLogins:AllLogins",
"AboutLogins:LoginAdded",
"AboutLogins:LoginModified",
"AboutLogins:LoginRemoved",
],
},
},
AboutReader: {
child: {
module: "resource:///actors/AboutReaderChild.jsm",
......@@ -448,6 +465,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
// eslint-disable-next-line no-unused-vars
XPCOMUtils.defineLazyModuleGetters(this, {
AboutLoginsParent: "resource:///modules/AboutLoginsParent.jsm",
AsyncPrefs: "resource://gre/modules/AsyncPrefs.jsm",
ContentClick: "resource:///modules/ContentClick.jsm",
FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
......@@ -526,6 +544,8 @@ const listeners = {
},
mm: {
"AboutLogins:DeleteLogin": ["AboutLoginsParent"],
"AboutLogins:Subscribe": ["AboutLoginsParent"],
"Content:Click": ["ContentClick"],
"ContentSearch": ["ContentSearch"],
"FormValidation:ShowPopup": ["FormValidationHandler"],
......
......@@ -22,6 +22,7 @@ namespace browser {
NS_IMPL_ISUPPORTS(AboutRedirector, nsIAboutModule)
bool AboutRedirector::sNewTabPageEnabled = false;
bool AboutRedirector::sAboutLoginsEnabled = false;
static const uint32_t ACTIVITY_STREAM_FLAGS =
nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::ENABLE_INDEXED_DB |
......@@ -56,6 +57,10 @@ static const RedirEntry kRedirMap[] = {
{"framecrashed", "chrome://browser/content/aboutFrameCrashed.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT},
{"logins", "chrome://browser/content/aboutlogins/aboutLogins.html",
nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGED_CHILD |
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT},
{"tabcrashed", "chrome://browser/content/aboutTabCrashed.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
......@@ -147,6 +152,13 @@ AboutRedirector::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
sNTPEnabledCacheInited = true;
}
static bool sAboutLoginsCacheInited = false;
if (!sAboutLoginsCacheInited) {
Preferences::AddBoolVarCache(&AboutRedirector::sAboutLoginsEnabled,
"signon.management.page.enabled");
sAboutLoginsCacheInited = true;
}
for (auto& redir : kRedirMap) {
if (!strcmp(path.get(), redir.id)) {
nsAutoCString url;
......@@ -162,6 +174,10 @@ AboutRedirector::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
NS_ENSURE_SUCCESS(rv, rv);
}
if (!sAboutLoginsEnabled && path.EqualsLiteral("logins")) {
return NS_ERROR_NOT_AVAILABLE;
}
if (path.EqualsLiteral("welcome")) {
nsCOMPtr<nsIAboutNewTabService> aboutNewTabService =
do_GetService("@mozilla.org/browser/aboutnewtab-service;1", &rv);
......
......@@ -24,6 +24,7 @@ class AboutRedirector : public nsIAboutModule {
virtual ~AboutRedirector() {}
private:
static bool sAboutLoginsEnabled;
static bool sNewTabPageEnabled;
};
......
......@@ -11,6 +11,7 @@ pages = [
'framecrashed',
'home',
'library',
'logins',
'newinstall',
'newtab',
'pocket-saved',
......
/* This 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["AboutLoginsChild"];
const {ActorChild} = ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
const {LoginHelper} = ChromeUtils.import("resource://gre/modules/LoginHelper.jsm");
class AboutLoginsChild extends ActorChild {
handleEvent(event) {
switch (event.type) {
case "AboutLoginsInit": {
this.mm.sendAsyncMessage("AboutLogins:Subscribe");
let waivedContent = Cu.waiveXrays(this.content);
let AboutLoginsUtils = {
doLoginsMatch(loginA, loginB) {
return LoginHelper.doLoginsMatch(loginA, loginB, {});
},
};
waivedContent.AboutLoginsUtils = Cu.cloneInto(AboutLoginsUtils, waivedContent, {
cloneFunctions: true,
});
break;
}
case "AboutLoginsDeleteLogin": {
this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
break;
}
}
}
receiveMessage(message) {
switch (message.name) {
case "AboutLogins:AllLogins":
this.sendToContent("AllLogins", message.data);
break;
case "AboutLogins:LoginAdded":
this.sendToContent("LoginAdded", message.data);
break;
case "AboutLogins:LoginModified":
this.sendToContent("LoginModified", message.data);
break;
case "AboutLogins:LoginRemoved":
this.sendToContent("LoginRemoved", message.data);
break;
}
}
sendToContent(messageType, detail) {
let message = Object.assign({messageType}, {value: detail});
let event = new this.content.CustomEvent("AboutLoginsChromeToContent", {
detail: Cu.cloneInto(message, this.content),
});
this.content.dispatchEvent(event);
}
}
/* This 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
ChromeUtils.defineModuleGetter(this, "LoginHelper",
"resource://gre/modules/LoginHelper.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const ABOUT_LOGINS_ORIGIN = "about:logins";
const isValidLogin = login => {
return !(login.hostname || "").startsWith("chrome://");
};
const convertSubjectToLogin = subject => {
subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
const login = LoginHelper.loginToVanillaObject(subject);
if (!isValidLogin(login)) {
return null;
}
return login;
};
var AboutLoginsParent = {
_subscribers: new WeakSet(),
// Listeners are added in BrowserGlue.jsm
receiveMessage(message) {
// Only respond to messages sent from about:logins.
if (message.target.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
return;
}
switch (message.name) {
case "AboutLogins:DeleteLogin": {
let login = LoginHelper.vanillaObjectToLogin(message.data.login);
Services.logins.removeLogin(login);
break;
}
case "AboutLogins:Subscribe": {
if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
Services.obs.addObserver(this, "passwordmgr-storage-changed");
}
this._subscribers.add(message.target);
let messageManager = message.target.messageManager;
messageManager.sendAsyncMessage("AboutLogins:AllLogins", this.getAllLogins());
break;
}
}
},
observe(subject, topic, type) {
if (!ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers).length) {
Services.obs.removeObserver(this, "passwordmgr-storage-changed");
return;
}
switch (type) {
case "addLogin": {
const login = convertSubjectToLogin(subject);
if (!login) {
return;
}
this.messageSubscribers("AboutLogins:LoginAdded", login);
break;
}
case "modifyLogin": {
subject.QueryInterface(Ci.nsIArrayExtensions);
const login = convertSubjectToLogin(subject.GetElementAt(1));
if (!login) {
return;
}
this.messageSubscribers("AboutLogins:LoginModified", login);
break;
}
case "removeLogin": {
const login = convertSubjectToLogin(subject);
if (!login) {
return;
}
this.messageSubscribers("AboutLogins:LoginRemoved", login);
}
default: {
break;
}
}
},
messageSubscribers(name, details) {
let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers);
for (let subscriber of subscribers) {
if (subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
this._subscribers.delete(subscriber);
continue;
}
try {
subscriber.messageManager.sendAsyncMessage(name, details);
} catch (ex) {}
}
},
getAllLogins() {
return Services.logins
.getAllLogins()
.filter(isValidLogin)
.map(LoginHelper.loginToVanillaObject);
},
};
# This 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/.
### This file is not in a locales directory to prevent it from
### being translated as the feature is still in heavy development
### and strings are likely to change often.
### Fluent isn't translating elements in the shadow DOM so the translated strings
### need to be applied to the composed node where they can be moved to the proper
### descendant after translation.
about-logins-page-title = Login Manager
login-list =
.login-list-header = Logins
login-item =
.login-item-hostname = Hostname
.login-item-password = Password
.login-item-username = Username
.login-item-time-created = Time Created
.login-item-delete = Delete
<!-- This 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/. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';"/>
<title data-l10n-id="about-logins-page-title"></title>
<link rel="localization" href="browser/aboutLogins.ftl">
<script defer="defer" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
<script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
<script defer="defer" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
<script defer="defer" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
</head>
<body>
<login-list data-l10n-id="login-list"
data-l10n-attrs="login-list-header"></login-list>
<login-item data-l10n-id="login-item"
data-l10n-attrs="login-item-hostname, login-item-password, login-item-username, login-item-time-created, login-item-delete"></login-item>
<template id="login-list-template">
<h2></h2>
<pre>
</pre>
</template>
<template id="login-list-item-template">
<style>
:host(.selected) {
font-weight: bold;
}
</style>
<span class="login-list-item-hostname"></span>
<span class="login-list-item-username"></span>
</template>
<template id="login-item-template">
<h2 data-l10n-id="login-item-header"></h2>
<label>
<span class="hostname-label"></span>
<input name="hostname"/>
</label>
<label>
<span class="username-label"></span>
<input name="username"/>
</label>
<label>
<span class="password-label"></span>
<input type="password" name="password"/>
</label>
<p>
<span class="time-created-label"></span>
<span class="time-created"></span>
</p>
<button class="delete-button"></button>
</template>
</body>
</html>
/* This 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/. */
let gElements = {};
document.addEventListener("DOMContentLoaded", () => {
gElements.loginList = document.querySelector("login-list");
gElements.loginItem = document.querySelector("login-item");
document.dispatchEvent(new CustomEvent("AboutLoginsInit", {bubbles: true}));
}, {once: true});
window.addEventListener("AboutLoginsChromeToContent", event => {
switch (event.detail.messageType) {
case "AllLogins": {
gElements.loginList.setLogins(event.detail.value);
break;
}
case "LoginAdded": {
gElements.loginList.loginAdded(event.detail.value);
gElements.loginItem.loginAdded(event.detail.value);
break;
}
case "LoginModified": {
gElements.loginList.loginModified(event.detail.value);
gElements.loginItem.loginModified(event.detail.value);
break;
}
case "LoginRemoved": {
gElements.loginList.loginRemoved(event.detail.value);
gElements.loginItem.loginRemoved(event.detail.value);
break;
}
}
});
/* This 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/. */
class LoginItem extends HTMLElement {
constructor() {
super();
this._login = {};
}
connectedCallback() {
if (this.children.length) {
this.render();
return;
}
let loginItemTemplate = document.querySelector("#login-item-template");
this.attachShadow({mode: "open"})
.appendChild(loginItemTemplate.content.cloneNode(true));
let deleteButton = this.shadowRoot.querySelector(".delete-button");
deleteButton.addEventListener("click", this);
window.addEventListener("AboutLoginsLoginSelected", this);
this.render();
}
static get observedAttributes() {
return [
"login-item-delete",
"login-item-hostname",
"login-item-password",
"login-item-time-created",
"login-item-username",
];
}
/* Fluent doesn't handle localizing into Shadow DOM yet so strings
need to get reflected in to their targeted element. */
attributeChangedCallback(attr, oldValue, newValue) {
if (!this.shadowRoot) {
return;
}
switch (attr) {
case "login-item-delete":
this.shadowRoot.querySelector(".delete-button").textContent = newValue;
break;
case "login-item-hostname":
this.shadowRoot.querySelector(".hostname-label").textContent = newValue;
break;
case "login-item-password":
this.shadowRoot.querySelector(".password-label").textContent = newValue;
break;
case "login-item-time-created":
this.shadowRoot.querySelector(".time-created-label").textContent = newValue;
break;
case "login-item-username":
this.shadowRoot.querySelector(".username-label").textContent = newValue;
break;
}
}
render() {
this.shadowRoot.querySelector("input[name='hostname']").value = this._login.hostname || "";
this.shadowRoot.querySelector("input[name='username']").value = this._login.username || "";
this.shadowRoot.querySelector("input[name='password']").value = this._login.password || "";
this.shadowRoot.querySelector(".time-created").textContent = this._login.timeCreated || "";
}
handleEvent(event) {
switch (event.type) {
case "AboutLoginsLoginSelected": {
this._login = event.detail;
this.render();
break;
}
case "click": {
if (event.target.classList.contains("delete-button")) {
document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
bubbles: true,
detail: this._login,
}));
}
break;
}
}
}
loginAdded(login) {
if (!this._login.guid) {
let tempLogin = {
username: this.shadowRoot.querySelector("input[name='username']").value,
formSubmitURL: "", // Use the wildcard since the user doesn't supply it.
hostname: this.shadowRoot.querySelector("input[name='hostname']").value,
password: this.shadowRoot.querySelector("input[name='password']").value,
};
// Need to use LoginHelper.doLoginsMatch() to see if the login
// that was added is the login that was being edited, so we
// can update time-created, etc.
if (window.AboutLoginsUtils.doLoginsMatch(tempLogin, login)) {
this._login = login;
this.render();
}
} else if (login.guid == this._login.guid) {
this._login = login;
this.render();
}
}
loginModified(login) {
if (login.guid != this._login.guid) {
return;
}
this._login = login;
this.render();
}
loginRemoved(login) {
if (login.guid != this._login.guid) {
return;
}
this._login = {};
this.render();
}
}
customElements.define("login-item", LoginItem);
/* This 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/. */
class LoginListItem extends HTMLElement {
constructor(login) {
super();
this._login = login;
this._selected = false;
}
connectedCallback() {
if (this.children.length) {
this.render();
return;
}
let loginListItemTemplate = document.querySelector("#login-list-item-template");
this.attachShadow({mode: "open"})
.appendChild(loginListItemTemplate.content.cloneNode(true));
this.render();
this.addEventListener("click", this);
window.addEventListener("AboutLoginsLoginSelected", this);
}
render() {
this.classList.toggle("selected", this._selected);
this.setAttribute("guid", this._login.guid);
this.shadowRoot.querySelector(".login-list-item-hostname").textContent = this._login.hostname;
this.shadowRoot.querySelector(".login-list-item-username").textContent = this._login.username;
}
handleEvent(event) {
switch (event.type) {
case "AboutLoginsLoginSelected": {
if (this._selected != (event.detail.guid == this._login.guid)) {
this._selected = event.detail.guid == this._login.guid;
this.render();
}
break;
}
case "click": {
this.dispatchEvent(new CustomEvent("AboutLoginsLoginSelected", {
bubbles: true,
composed: true,
detail: this._login,
}));
}
}
}
update(login) {
this._login = login;
this.render();
}
}
customElements.define("login-list-item", LoginListItem);
/* This 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/. */