Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import { DefaultAggregator } from "resource://gre/modules/megalist/aggregator/DefaultAggregator.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
});
/**
*
* Responsible for filtering lines data from the Aggregator and receiving user
* commands from the view.
*
* Refers to the same MegalistAggregator in the parent process to access data.
* Paired to exactly one MegalistView in the child process to present to the user.
*
*/
export class MegalistViewModel {
/**
*
* View Model prepares snapshots in the parent process to be displayed
* by the View in the child process.
*
* View requests line data by providing snapshotId.
*
*/
#snapshots = [];
#searchText = "";
#messageToView;
#authExpirationTime;
#aggregator = new DefaultAggregator();
constructor(messageToView) {
this.#messageToView = messageToView;
this.#authExpirationTime = Number.NEGATIVE_INFINITY;
this.#aggregator.attachViewModel(this);
}
get authExpirationTime() {
return this.#authExpirationTime;
}
willDestroy() {
this.#aggregator.detachViewModel(this);
this.#aggregator = null;
}
refreshAllLinesOnScreen() {
this.#rebuildSnapshots();
}
refreshSingleLineOnScreen(line) {
const snapshotIndex = this.#snapshots.indexOf(line);
if (snapshotIndex >= 0) {
const snapshot = this.#processSnapshotView(line, snapshotIndex);
this.#messageToView("Snapshot", {
snapshotId: snapshotIndex,
snapshot,
});
}
}
/**
*
* Send snapshot of necessary line data across parent-child boundary.
*
* @param {object} snapshotData
* @param {number} index
*/
#processSnapshotView(snapshotData, index) {
const snapshot = {
label: snapshotData.label,
value: snapshotData.value,
field: snapshotData.field,
lineIndex: index,
};
if ("commands" in snapshotData) {
snapshot.commands = snapshotData.commands;
}
if ("valueIcon" in snapshotData) {
snapshot.valueIcon = snapshotData.valueIcon;
}
if ("href" in snapshotData) {
snapshot.href = snapshotData.href;
}
if ("breached" in snapshotData) {
snapshot.breached = snapshotData.breached;
}
if ("breachedNotification" in snapshotData) {
snapshot.breachedNotification = snapshotData.breachedNotification;
}
if ("vulnerable" in snapshotData) {
snapshot.vulnerable = snapshotData.vulnerable;
}
if ("vulnerableNotification" in snapshotData) {
snapshot.vulnerableNotification = snapshotData.vulnerableNotification;
}
if ("noUsernameNotification" in snapshotData) {
snapshot.noUsernameNotification = snapshotData.noUsernameNotification;
}
if ("toggleTooltip" in snapshotData) {
snapshot.toggleTooltip = snapshotData.toggleTooltip;
}
if ("concealed" in snapshotData) {
snapshot.concealed = snapshotData.concealed;
}
if ("record" in snapshotData) {
snapshot.guid = snapshotData.record.guid;
}
return snapshot;
}
handleViewMessage({ name, data }) {
const handlerName = `receive${name}`;
if (!(handlerName in this)) {
throw new Error(`Received unknown message "${name}"`);
}
return this[handlerName](data);
}
receiveRefresh() {
this.#rebuildSnapshots();
}
#rebuildSnapshots() {
this.#snapshots = Array.from(
this.#aggregator.enumerateLines(this.#searchText)
);
// Expose relevant line properties to view
const viewSnapshots = this.#snapshots.map((snapshot, i) =>
this.#processSnapshotView(snapshot, i)
);
// Update snapshots on screen
this.#messageToView("ShowSnapshots", {
snapshots: viewSnapshots,
});
}
receiveUpdateFilter({ searchText } = { searchText: "" }) {
if (this.#searchText != searchText) {
this.#searchText = searchText;
this.#rebuildSnapshots();
}
}
async receiveReauthPrimaryPassword() {
const isAuthorized = await this.#promptForReauth("ReauthPrimaryPassword");
if (isAuthorized) {
Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
}
}
setNotification(notification) {
this.#messageToView("SetNotification", notification);
}
setDisplayMode(displayMode) {
this.#messageToView("SetDisplayMode", displayMode);
}
discardChangesConfirmed() {
this.#messageToView("DiscardChangesConfirmed");
}
setPrimaryPasswordAuthenticated(isAuthenticated) {
this.#messageToView("PrimaryPasswordAuthenticated", isAuthenticated);
}
async receiveCommand({ commandId, snapshotId, value } = {}) {
const dotIndex = commandId?.indexOf(".");
const index = snapshotId;
const snapshot = this.#snapshots[index];
if (dotIndex >= 0) {
const dataSourceName = commandId.substring(0, dotIndex);
const functionName = commandId.substring(dotIndex + 1);
this.#aggregator.callFunction(
dataSourceName,
functionName,
snapshot?.record
);
return;
}
if (snapshot) {
const commands = snapshot.commands;
commandId = commandId ?? commands[0]?.id;
const command = snapshot.commands.find(c => c.id == commandId);
if (!command?.verify || (await this.#promptForReauth(command))) {
await snapshot[`execute${commandId}`]?.(value);
}
}
}
async #promptForReauth(command) {
// used for recording telemetry
const reasonMap = {
Copy: "copy_cpm",
Reveal: "reveal_cpm",
Edit: "edit_cpm",
ReauthPrimaryPassword: "reauth_cpm",
};
const reason = reasonMap[command.id];
const osAuthForPw = lazy.LoginHelper.getOSAuthEnabled();
const { isAuthorized } = await lazy.LoginHelper.requestReauth(
lazy.BrowserWindowTracker.getTopWindow({
allowFromInactiveWorkspace: true,
}).gBrowser,
osAuthForPw,
this.#authExpirationTime,
command.OSAuthPromptMessage,
command.OSAuthCaptionMessage,
reason
);
if (isAuthorized) {
const authTimeoutMs = this.#aggregator.callFunction(
"LoginDataSource",
"getAuthTimeoutMs"
);
this.#authExpirationTime = Date.now() + authTimeoutMs;
}
this.#messageToView("ReauthResponse", isAuthorized);
return isAuthorized;
}
}