/* 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/. */ const EXPORTED_SYMBOLS = ["AddrBookCard"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; XPCOMUtils.defineLazyModuleGetters(lazy, { BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm", newUID: "resource:///modules/AddrBookUtils.jsm", VCardProperties: "resource:///modules/VCardUtils.jsm", VCardPropertyEntry: "resource:///modules/VCardUtils.jsm", }); /** * Prototype for nsIAbCard objects that are not mailing lists. * * @implements {nsIAbCard} */ function AddrBookCard() { this._directoryUID = ""; this._properties = new Map([ ["PopularityIndex", 0], ["LastModifiedDate", 0], ]); this._hasVCard = false; XPCOMUtils.defineLazyGetter(this, "_vCardProperties", () => { // Lazy creation of the VCardProperties object. Change the `_properties` // object as much as you like (e.g. loading in properties from a database) // before running this code. After it runs, the `_vCardProperties` object // takes over and anything in `_properties` which could be stored in the // vCard will be ignored! this._hasVCard = true; let vCard = this.getProperty("_vCard", ""); try { if (vCard) { let vCardProperties = lazy.VCardProperties.fromVCard(vCard, { isGoogleCardDAV: this._isGoogleCardDAV, }); // Custom1..4 properties could still exist as nsIAbCard properties. // Migrate them now. for (let key of ["Custom1", "Custom2", "Custom3", "Custom4"]) { let value = this.getProperty(key, ""); if ( value && vCardProperties.getFirstEntry(`x-${key.toLowerCase()}`) === null ) { vCardProperties.addEntry( new lazy.VCardPropertyEntry( `x-${key.toLowerCase()}`, {}, "text", value ) ); } this.deleteProperty(key); } return vCardProperties; } return lazy.VCardProperties.fromPropertyMap(this._properties); } catch (error) { console.error("Error creating vCard properties", error); // Return an empty VCardProperties object if parsing failed // catastrophically. return new lazy.VCardProperties("4.0"); } }); } AddrBookCard.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIAbCard"]), classID: Components.ID("{1143991d-31cd-4ea6-9c97-c587d990d724}"), /* nsIAbCard */ generateName(generateFormat, bundle) { let result = ""; switch (generateFormat) { case Ci.nsIAbCard.GENERATE_DISPLAY_NAME: result = this.displayName; break; case Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER: if (this.lastName) { let otherNames = [ this.prefixName, this.firstName, this.middleName, this.suffixName, ] .filter(Boolean) .join(" "); if (!otherNames) { // Only use the lastName if we don't have anything to add after the // comma, in order to avoid for the string to finish with ", ". result = this.lastName; } else { result = bundle?.formatStringFromName("lastFirstFormat", [ this.lastName, otherNames, ]) ?? `${this.lastName}, ${otherNames}`; } } break; default: let startNames = [this.prefixName, this.firstName, this.middleName] .filter(Boolean) .join(" "); let endNames = [this.lastName, this.suffixName] .filter(Boolean) .join(" "); result = bundle?.formatStringFromName("firstLastFormat", [ startNames, endNames, ]) ?? `${startNames} ${endNames}`; break; } // Remove any leftover blank spaces. result = result.trim(); if (result == "" || result == ",") { result = this.displayName || [ this.prefixName, this.firstName, this.middleName, this.lastName, this.suffixName, ] .filter(Boolean) .join(" ") .trim(); if (!result) { // So far we don't have anything to show as a contact name. if (this.primaryEmail) { // Let's use the primary email localpart. result = this.primaryEmail.split("@", 1)[0]; } else { // We don't have a primary email either, let's try with the // organization name. result = !this._hasVCard ? this.getProperty("Company", "") : this._vCardProperties.getFirstValue("org"); } } } return result || ""; }, get directoryUID() { return this._directoryUID; }, set directoryUID(value) { this._directoryUID = value; }, get UID() { if (!this._uid) { this._uid = lazy.newUID(); } return this._uid; }, set UID(value) { if (this._uid && value != this._uid) { throw Components.Exception( `Bad UID: got ${value} != ${this.uid}`, Cr.NS_ERROR_UNEXPECTED ); } this._uid = value; }, get properties() { let props = []; for (const [name, value] of this._properties) { props.push({ get name() { return name; }, get value() { return value; }, QueryInterface: ChromeUtils.generateQI(["nsIProperty"]), }); } return props; }, get supportsVCard() { return true; }, get vCardProperties() { return this._vCardProperties; }, get firstName() { if (!this._hasVCard) { return this.getProperty("FirstName", ""); } let name = this._vCardProperties.getFirstValue("n"); if (!Array.isArray(name)) { return ""; } name = name[1]; if (Array.isArray(name)) { name = name.join(" "); } return name; }, set firstName(value) { let n = this._vCardProperties.getFirstEntry("n"); if (n) { n.value[1] = value; } else { this._vCardProperties.addEntry( new lazy.VCardPropertyEntry("n", {}, "text", ["", value, "", "", ""]) ); } }, get lastName() { if (!this._hasVCard) { return this.getProperty("LastName", ""); } let name = this._vCardProperties.getFirstValue("n"); if (!Array.isArray(name)) { return ""; } name = name[0]; if (Array.isArray(name)) { name = name.join(" "); } return name; }, set lastName(value) { let n = this._vCardProperties.getFirstEntry("n"); if (n) { n.value[0] = value; } else { this._vCardProperties.addEntry( new lazy.VCardPropertyEntry("n", {}, "text", [value, "", "", "", ""]) ); } }, get displayName() { if (!this._hasVCard) { return this.getProperty("DisplayName", ""); } return this._vCardProperties.getFirstValue("fn") || ""; }, set displayName(value) { let fn = this._vCardProperties.getFirstEntry("fn"); if (fn) { fn.value = value; } else { this._vCardProperties.addEntry( new lazy.VCardPropertyEntry("fn", {}, "text", value) ); } }, get primaryEmail() { if (!this._hasVCard) { return this.getProperty("PrimaryEmail", ""); } return this._vCardProperties.getAllValuesSorted("email")[0] ?? ""; }, set primaryEmail(value) { let entries = this._vCardProperties.getAllEntriesSorted("email"); if (entries.length && entries[0].value != value) { this._vCardProperties.removeEntry(entries[0]); entries.shift(); } if (value) { let existing = entries.find(e => e.value == value); if (existing) { existing.params.pref = "1"; } else { this._vCardProperties.addEntry( new lazy.VCardPropertyEntry("email", { pref: "1" }, "text", value) ); } } else if (entries.length) { entries[0].params.pref = "1"; } }, get isMailList() { return false; }, get mailListURI() { return ""; }, get emailAddresses() { return this._vCardProperties.getAllValuesSorted("email"); }, get photoURL() { let photoEntry = this.vCardProperties.getFirstEntry("photo"); if (photoEntry?.value) { if (photoEntry.value?.startsWith("data:image/")) { // This is a version 4.0 card // OR a version 3.0 card with the URI type set (uncommon) // OR a version 3.0 card that is lying about its type. return photoEntry.value; } if (photoEntry.type == "binary" && photoEntry.value.startsWith("iVBO")) { // This is a version 3.0 card. // The first 3 bytes say this image is PNG. return `data:image/png;base64,${photoEntry.value}`; } if (photoEntry.type == "binary" && photoEntry.value.startsWith("/9j/")) { // This is a version 3.0 card. // The first 3 bytes say this image is JPEG. return `data:image/jpeg;base64,${photoEntry.value}`; } if (photoEntry.type == "uri" && /^https?:\/\//.test(photoEntry.value)) { // A remote URI. return photoEntry.value; } } let photoName = this.getProperty("PhotoName", ""); if (photoName) { let file = Services.dirsvc.get("ProfD", Ci.nsIFile); file.append("Photos"); file.append(photoName); return Services.io.newFileURI(file).spec; } return ""; }, getProperty(name, defaultValue) { if (this._properties.has(name)) { return this._properties.get(name); } return defaultValue; }, getPropertyAsAString(name) { if (!this._properties.has(name)) { return ""; } return this.getProperty(name); }, getPropertyAsAUTF8String(name) { if (!this._properties.has(name)) { throw Components.Exception(`${name} N/A`, Cr.NS_ERROR_NOT_AVAILABLE); } return this.getProperty(name); }, getPropertyAsUint32(name) { let value = this.getProperty(name); if (!isNaN(parseInt(value, 10))) { return parseInt(value, 10); } if (!isNaN(parseInt(value, 16))) { return parseInt(value, 16); } throw Components.Exception( `${name}: ${value} - not an int`, Cr.NS_ERROR_NOT_AVAILABLE ); }, getPropertyAsBool(name, defaultValue) { let value = this.getProperty(name); switch (value) { case false: case 0: case "0": return false; case true: case 1: case "1": return true; case undefined: return defaultValue; } throw Components.Exception( `${name}: ${value} - not a boolean`, Cr.NS_ERROR_NOT_AVAILABLE ); }, setProperty(name, value) { if (lazy.BANISHED_PROPERTIES.includes(name)) { throw new Components.Exception( `Unable to set ${name} as a property, use vCardProperties`, Cr.NS_ERROR_UNEXPECTED ); } if ([null, undefined, ""].includes(value)) { this._properties.delete(name); return; } if (typeof value == "boolean") { value = value ? "1" : "0"; } this._properties.set(name, "" + value); }, setPropertyAsAString(name, value) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); }, setPropertyAsAUTF8String(name, value) { this.setProperty(name, value); }, setPropertyAsUint32(name, value) { this.setProperty(name, value); }, setPropertyAsBool(name, value) { this.setProperty(name, value ? "1" : "0"); }, deleteProperty(name) { this._properties.delete(name); }, hasEmailAddress(emailAddress) { emailAddress = emailAddress.toLowerCase(); return this.emailAddresses.some(e => e.toLowerCase() == emailAddress); }, translateTo(type) { if (type == "vcard") { if (!this._vCardProperties.getFirstValue("uid")) { this._vCardProperties.addValue("uid", this.UID); } return encodeURIComponent(this._vCardProperties.toVCard()); } // Get nsAbCardProperty to do the work, the code is in C++ anyway. let cardCopy = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( Ci.nsIAbCard ); cardCopy.UID = this.UID; cardCopy.copy(this); return cardCopy.translateTo(type); }, generatePhoneticName(lastNameFirst) { if (lastNameFirst) { return ( this.getProperty("PhoneticLastName", "") + this.getProperty("PhoneticFirstName", "") ); } return ( this.getProperty("PhoneticFirstName", "") + this.getProperty("PhoneticLastName", "") ); }, generateChatName() { for (let name of [ "_GoogleTalk", "_AimScreenName", "_Yahoo", "_Skype", "_QQ", "_MSN", "_ICQ", "_JabberId", "_IRC", ]) { if (this._properties.has(name)) { return this._properties.get(name); } } return ""; }, copy(srcCard) { throw Components.Exception( "nsIAbCard.copy() not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED ); }, equals(card) { return this.UID == card.UID; }, };