/* 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 { IMServices } from "resource:///modules/IMServices.sys.mjs"; import { OTR } from "resource:///modules/OTR.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyGetter( lazy, "l10n", () => new Localization(["messenger/otr/otrUI.ftl"], true) ); function _str(id) { return lazy.l10n.formatValueSync(id); } function _strArgs(id, args) { return lazy.l10n.formatValueSync(id, args); } const OTR_ADD_FINGER_DIALOG_URL = "chrome://chat/content/otr-add-fingerprint.xhtml"; const AUTH_STATUS_UNVERIFIED = "otr-auth-unverified"; var authLabelMap; var trustMap; function initStrings() { authLabelMap = new Map([ ["otr:auth-error", _str("auth-error")], ["otr:auth-success", _str("auth-success")], ["otr:auth-success-them", _str("auth-success-them")], ["otr:auth-fail", _str("auth-fail")], ["otr:auth-waiting", _str("auth-waiting")], ]); let sl = _str("start-label"); let al = _str("auth-label"); let rfl = _str("refresh-label"); let ral = _str("reauth-label"); trustMap = new Map([ [ OTR.trustState.TRUST_NOT_PRIVATE, { startLabel: sl, authLabel: al, disableStart: false, disableEnd: true, disableAuth: true, class: "not-private", }, ], [ OTR.trustState.TRUST_UNVERIFIED, { startLabel: rfl, authLabel: al, disableStart: false, disableEnd: false, disableAuth: false, class: "unverified", }, ], [ OTR.trustState.TRUST_PRIVATE, { startLabel: rfl, authLabel: ral, disableStart: false, disableEnd: false, disableAuth: false, class: "private", }, ], [ OTR.trustState.TRUST_FINISHED, { startLabel: sl, authLabel: al, disableStart: false, disableEnd: false, disableAuth: true, class: "finished", }, ], ]); } var windowRefs = new Map(); export var OTRUI = { enabled: false, stringsLoaded: false, globalDoc: null, visibleConv: null, debug: false, logMsg(msg) { if (!OTRUI.debug) { return; } Services.console.logStringMessage(msg); }, addMenuObserver() { for (let win of Services.ww.getWindowEnumerator()) { OTRUI.addMenus(win); } Services.obs.addObserver(OTRUI, "domwindowopened"); }, removeMenuObserver() { for (let win of Services.ww.getWindowEnumerator()) { OTRUI.removeMenus(win); } Services.obs.removeObserver(OTRUI, "domwindowopened"); }, addMenus(win) { let doc = win.document; // Account for unready windows if (doc.readyState !== "complete") { let listen = function () { win.removeEventListener("load", listen); OTRUI.addMenus(win); }; win.addEventListener("load", listen); } }, removeMenus(win) { let doc = win.document; OTRUI.removeBuddyContextMenu(doc); }, addBuddyContextMenu(buddyContextMenu, doc, contact) { if (!buddyContextMenu || !OTR.libLoaded) { return; // Not the buddy list context menu } let sep = doc.createXULElement("menuseparator"); sep.setAttribute("id", "otrsep"); let menuitem = doc.createXULElement("menuitem"); menuitem.setAttribute("label", _str("buddycontextmenu-label")); menuitem.setAttribute("id", "otrcont"); menuitem.addEventListener("command", () => { let args = OTRUI.contactWrapper(contact); args.wrappedJSObject = args; let features = "chrome,modal,centerscreen,resizable=no,minimizable=no"; Services.ww.openWindow( null, OTR_ADD_FINGER_DIALOG_URL, "", features, args ); }); buddyContextMenu.addEventListener("popupshowing", e => { let target = e.target.triggerNode; if (target.localName == "richlistitem") { menuitem.hidden = false; sep.hidden = false; } else { /* probably imconv */ menuitem.hidden = true; sep.hidden = true; } }); buddyContextMenu.appendChild(sep); buddyContextMenu.appendChild(menuitem); }, removeBuddyContextMenu(doc) { let s = doc.getElementById("otrsep"); if (s) { s.remove(); } let p = doc.getElementById("otrcont"); if (p) { p.remove(); } }, loopKeyGenSuccess() { ChromeUtils.idleDispatch(OTRUI.genNextMissingKey); }, loopKeyGenFailure(param) { ChromeUtils.idleDispatch(OTRUI.genNextMissingKey); OTRUI.reportKeyGenFailure(param); }, reportKeyGenFailure(param) { throw new Error(_strArgs("otr-genkey-failed", { error: String(param) })); }, accountsToGenKey: [], genNextMissingKey() { if (OTRUI.accountsToGenKey.length == 0) { return; } let acc = OTRUI.accountsToGenKey.pop(); let fp = OTR.privateKeyFingerprint(acc.name, acc.prot); if (!fp) { OTR.generatePrivateKey(acc.name, acc.prot).then( OTRUI.loopKeyGenSuccess, OTRUI.loopKeyGenFailure ); } else { ChromeUtils.idleDispatch(OTRUI.genNextMissingKey); } }, genMissingKeys() { for (let acc of IMServices.accounts.getAccounts()) { OTRUI.accountsToGenKey.push({ name: acc.normalizedName, prot: acc.protocol.normalizedName, }); } ChromeUtils.idleDispatch(OTRUI.genNextMissingKey); }, async init() { if (!OTRUI.stringsLoaded) { // HACK: calling initStrings may fail the first time due to synchronous // loading of the .ftl files. If we load the files and wait for a known // value asynchronously, no such failure will happen. // // If the value "start-label" is removed, this will fail. // // Also, we can't reuse this Localization object elsewhere because it // fails to load values synchronously (even after calling setIsSync). await new Localization(["messenger/otr/otrUI.ftl"]).formatValue( "start-label" ); initStrings(); OTRUI.stringsLoaded = true; } this.debug = Services.prefs.getBoolPref("chat.otr.trace", false); OTR.init({}); if (!OTR.libLoaded) { return; } this.enabled = true; this.notificationbox = null; OTR.addObserver(OTRUI); OTR.loadFiles() .then(function () { Services.obs.addObserver(OTR, "new-ui-conversation"); Services.obs.addObserver(OTR, "conversation-update-type"); // Disabled until #76 is resolved. // Services.obs.addObserver(OTRUI, "contact-added", false); Services.obs.addObserver(OTRUI, "account-added"); // Services.obs.addObserver(OTRUI, "contact-signed-off", false); Services.obs.addObserver(OTRUI, "conversation-loaded"); Services.obs.addObserver(OTRUI, "conversation-closed"); Services.obs.addObserver(OTRUI, "prpl-quit"); for (let conv of IMServices.conversations.getConversations()) { OTRUI.initConv(conv); } OTRUI.addMenuObserver(); ChromeUtils.idleDispatch(OTRUI.genMissingKeys); }) .catch(function (err) { // console.log("===> " + err + "\n"); throw err; }); }, disconnect(aConv) { if (aConv) { return OTR.disconnect(aConv, true); } let allGood = true; for (let conv of IMServices.conversations.getConversations()) { if (conv.isChat) { continue; } if (!OTR.disconnect(conv, true)) { allGood = false; } } return allGood; }, openAuth(window, name, mode, uiConv, contactInfo) { let otrAuth = this.globalDoc.querySelector(".otr-auth"); otrAuth.disabled = true; let win = window.openDialog( "chrome://chat/content/otr-auth.xhtml", "auth=" + name, "centerscreen,resizable=no,minimizable=no", mode, uiConv, contactInfo ); windowRefs.set(name, win); window.addEventListener("beforeunload", function () { otrAuth.disabled = false; windowRefs.delete(name); }); }, closeAuth(context) { let win = windowRefs.get(context.username); if (win) { win.close(); } }, /** * Hide the encryption state container and any pending notifications. * * @param {Element} otrContainer * @param {Context} [context] */ noOtrPossible(otrContainer, context) { otrContainer.hidden = true; if (context) { OTRUI.hideUserNotifications(context); } else { OTRUI.hideAllOTRNotifications(); } }, sendSystemAlert(uiConv, conv, bundleId) { uiConv.systemMessage( _strArgs(bundleId, { name: conv.normalizedName }), false, true ); }, setNotificationBox(notificationbox) { this.globalBox = notificationbox; }, /* * These states are only relevant if OTR is the only encryption available for * the conversation. Protocol provided encryption takes priority. * possible states: * tab isn't a 1:1, isChat == true * then OTR isn't possible, hide the button * tab is a 1:1, isChat == false * no conversation active, uiConv cannot be found * then OTR isn't possible YET, hide the button * conversation active, uiConv found * disconnected? * could the other side come back? should we keep the button? * set the state based on the OTR library state */ /** * Store a reference to the document, as well as the current conversation. * * @param {Element} aObject - conversation-browser instance (most importantly, has a _conv field) */ addButton(aObject) { this.globalDoc = aObject.ownerDocument; let _conv = aObject._conv; OTRUI.visibleConv = _conv; if ( _conv.encryptionState === Ci.prplIConversation.ENCRYPTION_NOT_SUPPORTED ) { OTRUI.setMsgState(_conv, null, this.globalDoc, true); } }, /** * Hide the encryption state information for the current conversation. */ hideOTRButton() { if (!OTR.libLoaded) { return; } if (!this.globalDoc) { return; } OTRUI.visibleConv = null; let otrContainer = this.globalDoc.querySelector(".encryption-container"); OTRUI.noOtrPossible(otrContainer); }, /** * Sets the visible conversation of the OTR UI state and ensures * the encryption state button is set up correctly. * * @param {prplIConversation} _conv */ updateOTRButton(_conv) { if ( _conv.encryptionState !== Ci.prplIConversation.ENCRYPTION_NOT_SUPPORTED ) { return; } if (!OTR.libLoaded) { return; } if (!this.globalDoc) { return; } OTRUI.visibleConv = _conv; let convBinding; for (let element of this.globalDoc.getElementById("conversationsBox") .children) { if (!element.hidden) { convBinding = element; break; } } if (convBinding && convBinding._conv && convBinding._conv.target) { OTRUI.setMsgState(_conv, null, this.globalDoc, false); } else { this.hideOTRButton(); } }, /** * Set encryption state on selector for conversation. * * @param {prplIConversation} _conv - Must match the visible conversation. * @param {Context} [context] - The OTR context for the conversation. * @param {DOMDocument} doc * @param {boolean} [addSystemMessage] - If a system message with the conversation security. */ setMsgState(_conv, context, doc, addSystemMessage) { if (!this.visibleConv) { return; } if (_conv != null && !(_conv === this.visibleConv)) { return; } let otrContainer = doc.querySelector(".encryption-container"); let otrButton = doc.querySelector(".encryption-button"); if (_conv != null && _conv.isChat) { OTRUI.noOtrPossible(otrContainer, context); return; } if (!context && _conv != null) { context = OTR.getContext(_conv); if (!context) { OTRUI.noOtrPossible(otrContainer, null); } } try { let uiConv = OTR.getUIConvFromContext(context); if (uiConv != null && !(uiConv === this.visibleConv)) { return; } if ( uiConv.encryptionState === Ci.prplIConversation.ENCRYPTION_ENABLED || uiConv.encryptionState === Ci.prplIConversation.ENCRYPTION_TRUSTED ) { return; } if (uiConv.isChat) { OTRUI.noOtrPossible(otrContainer, context); return; } if (addSystemMessage) { let trust = OTRUI.getTrustSettings(context); let id = "state-" + trust.class; let msg; if (OTR.trust(context) == OTR.trustState.TRUST_NOT_PRIVATE) { msg = lazy.l10n.formatValueSync(id); } else { msg = lazy.l10n.formatValueSync(id, { name: context.username }); } uiConv.systemMessage(msg, false, true); } } catch (e) { OTRUI.noOtrPossible(otrContainer, context); return; } otrContainer.hidden = false; let otrStart = doc.querySelector(".otr-start"); let otrEnd = doc.querySelector(".otr-end"); let otrAuth = doc.querySelector(".otr-auth"); let trust = OTRUI.getTrustSettings(context); otrButton.setAttribute( "tooltiptext", _strArgs("state-" + trust.class, { name: context.username }) ); otrButton.setAttribute("label", _str("state-" + trust.class + "-label")); otrButton.className = "encryption-button encryption-" + trust.class; otrStart.setAttribute("label", trust.startLabel); otrStart.setAttribute("disabled", trust.disableStart); otrEnd.setAttribute("disabled", trust.disableEnd); otrAuth.setAttribute("label", trust.authLabel); otrAuth.setAttribute("disabled", trust.disableAuth); OTRUI.hideAllOTRNotifications(); OTRUI.showUserNotifications(context); }, alertTrust(context) { let uiConv = OTR.getUIConvFromContext(context); let trust = OTRUI.getTrustSettings(context); uiConv.systemMessage( _strArgs("afterauth-" + trust.class, { name: context.username }), false, true ); }, getTrustSettings(context) { let result = trustMap.get(OTR.trust(context)); return result; }, askAuth(aObject) { let uiConv = OTR.getUIConvFromContext(aObject.context); if (!uiConv) { return; } let name = uiConv.target.normalizedName; let msg = _strArgs("verify-request", { name }); // Trigger the update of the unread message counter. uiConv.notifyVerifyOTR(msg); Services.obs.notifyObservers(uiConv, "new-otr-verification-request"); let window = this.globalDoc.defaultView; let buttons = [ { label: _str("finger-verify"), accessKey: _str("finger-verify-access-key"), callback() { OTRUI.openAuth(window, name, "ask", uiConv, aObject); // prevent closing of notification bar when the button is hit return true; }, }, { label: _str("finger-ignore"), accessKey: _str("finger-ignore-access-key"), callback() { let context = OTR.getContext(uiConv.target); OTR.abortSMP(context); }, }, ]; let notification = this.globalBox.appendNotification( `ask-auth-${name}`, { label: msg, priority: this.globalBox.PRIORITY_WARNING_MEDIUM, }, buttons ); notification.removeAttribute("dismissable"); }, closeAskAuthNotification(aObject) { let name = aObject.context.username; let notification = this.globalBox.getNotificationWithValue( `ask-auth-${name}` ); if (!notification) { return; } this.globalBox.removeNotification(notification); }, closeUnverified(context) { let uiConv = OTR.getUIConvFromContext(context); if (!uiConv) { return; } for (let notification of this.globalBox.allNotifications) { if ( context.username == notification.getAttribute("user") && notification.getAttribute("value") == AUTH_STATUS_UNVERIFIED ) { notification.close(); } } }, hideUserNotifications(context) { for (let notification of this.globalBox.allNotifications) { if (context.username == notification.getAttribute("user")) { notification.close(); } } }, hideAllOTRNotifications() { for (let notification of this.globalBox.allNotifications) { if (notification.getAttribute("protocol") == "otr") { notification.setAttribute("hidden", "true"); } } }, showUserNotifications(context) { let name = context.username; for (let notification of this.globalBox.allNotifications) { if (name == notification.getAttribute("user")) { notification.removeAttribute("hidden"); } } }, notifyUnverified(context, seen) { let uiConv = OTR.getUIConvFromContext(context); if (!uiConv) { return; } let name = context.username; let window = this.globalDoc.defaultView; let buttons = [ { label: _str("finger-verify"), accessKey: _str("finger-verify-access-key"), callback() { let name = uiConv.target.normalizedName; OTRUI.openAuth(window, name, "start", uiConv); // prevent closing of notification bar when the button is hit return true; }, }, { label: _str("finger-ignore"), accessKey: _str("finger-ignore-access-key"), callback() { let context = OTR.getContext(uiConv.target); OTR.abortSMP(context); }, }, ]; let notification = this.globalBox.appendNotification( name, { label: _strArgs(`finger-${seen}`, { name }), priority: this.globalBox.PRIORITY_WARNING_MEDIUM, }, buttons ); // Set the user attribute so we can show and hide notifications based on the // currently viewed conversation. notification.setAttribute("user", name); // Set custom attributes for CSS styling. notification.setAttribute("protocol", "otr"); notification.setAttribute("status", AUTH_STATUS_UNVERIFIED); // Prevent users from dismissing this notification. notification.removeAttribute("dismissable"); if (!this.visibleConv) { return; } if (name !== this.visibleConv.normalizedName) { this.hideUserNotifications(context); } }, closeVerification(context) { let uiConv = OTR.getUIConvFromContext(context); if (!uiConv) { return; } let prevNotification = OTRUI.globalBox.getNotificationWithValue( context.username ); if (prevNotification) { prevNotification.close(); } }, notifyVerification(context, key, cancelable, verifiable) { let uiConv = OTR.getUIConvFromContext(context); if (!uiConv) { return; } OTRUI.closeVerification(context); let buttons = []; if (cancelable) { buttons = [ { label: _str("auth-cancel"), accessKey: _str("auth-cancel-access-key"), callback() { let context = OTR.getContext(uiConv.target); OTR.abortSMP(context); }, }, ]; } if (verifiable) { let window = this.globalDoc.defaultView; buttons = [ { label: _str("finger-verify"), accessKey: _str("finger-verify-access-key"), callback() { let name = uiConv.target.normalizedName; OTRUI.openAuth(window, name, "start", uiConv); // prevent closing of notification bar when the button is hit return true; }, }, { label: _str("finger-ignore"), accessKey: _str("finger-ignore-access-key"), callback() { let context = OTR.getContext(uiConv.target); OTR.abortSMP(context); }, }, ]; } // Change priority type based on the passed key. let priority = this.globalBox.PRIORITY_WARNING_HIGH; let dismissable = true; switch (key) { case "otr:auth-error": case "otr:auth-fail": priority = this.globalBox.PRIORITY_CRITICAL_HIGH; break; case "otr:auth-waiting": priority = this.globalBox.PRIORITY_INFO_MEDIUM; dismissable = false; break; default: break; } OTRUI.closeUnverified(context); let notification = this.globalBox.appendNotification( context.username, { label: authLabelMap.get(key), priority, }, buttons ); // Set the user attribute so we can show and hide notifications based on the // currently viewed conversation. notification.setAttribute("user", context.username); // Set custom attributes for CSS styling. notification.setAttribute("protocol", "otr"); notification.setAttribute("status", key); // The notification API don't currently support a "success" PRIORITY flag, // so we need to manually set it if we need to. if (["otr:auth-success", "otr:auth-success-them"].includes(key)) { notification.setAttribute("type", "success"); } if (!dismissable) { // Prevent users from dismissing this notification if something is in // progress or an action is required. notification.removeAttribute("dismissable"); } }, updateAuth(aObj) { // let uiConv = OTR.getUIConvFromContext(aObj.context); if (!aObj.progress) { OTRUI.closeAuth(aObj.context); OTRUI.notifyVerification(aObj.context, "otr:auth-error", false, false); } else if (aObj.progress === 100) { let key; let verifiable = false; if (aObj.success) { if (aObj.context.trust) { key = "otr:auth-success"; OTR.notifyTrust(aObj.context); } else { key = "otr:auth-success-them"; verifiable = true; } } else { key = "otr:auth-fail"; if (!aObj.context.trust) { OTR.notifyTrust(aObj.context); } } OTRUI.notifyVerification(aObj.context, key, false, verifiable); } else { // TODO: show the aObj.progress to the user with a // OTRUI.notifyVerification(aObj.context, "otr:auth-waiting", true, false); } OTRUI.closeAskAuthNotification(aObj); }, onAccountCreated(acc) { let account = acc.normalizedName; let protocol = acc.protocol.normalizedName; Promise.resolve(); if (OTR.privateKeyFingerprint(account, protocol) === null) { OTR.generatePrivateKey(account, protocol).catch( OTRUI.reportKeyGenFailure ); } }, contactWrapper(contact) { // If the conversation already started. if (contact.buddy) { return { account: contact.buddy.normalizedName, protocol: contact.buddy.buddy.protocol.normalizedName, screenname: contact.buddy.userName, }; } // For online and offline contacts without an open conversation. return { account: contact.preferredBuddy.preferredAccountBuddy.account.normalizedName, protocol: contact.preferredBuddy.protocol.normalizedName, screenname: contact.preferredBuddy.preferredAccountBuddy.userName, }; }, onContactAdded(contact) { let args = OTRUI.contactWrapper(contact); if ( OTR.getFingerprintsForRecipient( args.account, args.protocol, args.screenname ).length > 0 ) { return; } args.wrappedJSObject = args; let features = "chrome,modal,centerscreen,resizable=no,minimizable=no"; Services.ww.openWindow(null, OTR_ADD_FINGER_DIALOG_URL, "", features, args); }, observe(aObject, aTopic, aMsg) { let doc; // console.log("====> observing topic: " + aTopic + " with msg: " + aMsg); // console.log(aObject); switch (aTopic) { case "nsPref:changed": break; case "conversation-loaded": doc = aObject.ownerDocument; let windowtype = doc.documentElement.getAttribute("windowtype"); if (windowtype !== "mail:3pane") { return; } OTRUI.addButton(aObject); break; case "conversation-closed": if (aObject.isChat) { return; } this.globalBox.removeAllNotifications(); OTRUI.closeAuth(OTR.getContext(aObject)); OTRUI.disconnect(aObject); break; // case "contact-signed-off": // break; case "prpl-quit": OTRUI.disconnect(null); break; case "domwindowopened": OTRUI.addMenus(aObject); break; case "otr:generate": { let result = OTR.generatePrivateKeySync( aObject.account, aObject.protocol ); if (result != null) { OTRUI.reportKeyGenFailure(result); } break; } case "otr:disconnected": case "otr:msg-state": if ( aTopic === "otr:disconnected" || OTR.trust(aObject) !== OTR.trustState.TRUST_UNVERIFIED ) { OTRUI.closeAuth(aObject); OTRUI.closeUnverified(aObject); OTRUI.closeVerification(aObject); } OTRUI.setMsgState(null, aObject, this.globalDoc, false); break; case "otr:unverified": if (!this.globalDoc) { let win = Services.wm.getMostRecentWindow("mail:3pane"); if (!win) { return; } win.focus(); win.showChatTab(); this.globalDoc = win.document; } OTRUI.notifyUnverified(aObject, aMsg); break; case "otr:trust-state": OTRUI.alertTrust(aObject); break; case "otr:log": OTRUI.logMsg("otr: " + aObject); break; case "account-added": OTRUI.onAccountCreated(aObject); break; case "contact-added": OTRUI.onContactAdded(aObject); break; case "otr:auth-ask": OTRUI.askAuth(aObject); break; case "otr:auth-update": OTRUI.updateAuth(aObject); break; case "otr:cancel-ask-auth": OTRUI.closeAskAuthNotification(aObject); break; } }, initConv(binding) { OTR.addConversation(binding._conv); OTRUI.addButton(binding); }, /** * Restore the conversation to a state before OTR knew about it. * * @param {Element} binding - conversation-browser instance. */ resetConv(binding) { OTR.removeConversation(binding._conv); }, destroy() { if (!OTR.libLoaded) { return; } OTRUI.disconnect(null); Services.obs.removeObserver(OTR, "new-ui-conversation"); Services.obs.removeObserver(OTR, "conversation-update-type"); // Services.obs.removeObserver(OTRUI, "contact-added"); // Services.obs.removeObserver(OTRUI, "contact-signed-off"); Services.obs.removeObserver(OTRUI, "account-added"); Services.obs.removeObserver(OTRUI, "conversation-loaded"); Services.obs.removeObserver(OTRUI, "conversation-closed"); Services.obs.removeObserver(OTRUI, "prpl-quit"); for (let conv of IMServices.conversations.getConversations()) { OTRUI.resetConv(conv); } OTR.removeObserver(OTRUI); OTR.close(); OTRUI.removeMenuObserver(); }, };