diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mail/services/sync/modules/engines/addressBooks.sys.mjs | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs b/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs new file mode 100644 index 0000000000..9df9f678df --- /dev/null +++ b/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs @@ -0,0 +1,380 @@ +/* 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 { CryptoWrapper } from "resource://services-sync/record.sys.mjs"; +import { + Store, + SyncEngine, + Tracker, +} from "resource://services-sync/engines.sys.mjs"; +import { Utils } from "resource://services-sync/util.sys.mjs"; + +const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import( + "resource://services-sync/constants.js" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const SYNCED_COMMON_PROPERTIES = { + autocomplete: "enable_autocomplete", + readOnly: "readOnly", +}; + +const SYNCED_CARDDAV_PROPERTIES = { + syncInterval: "carddav.syncinterval", + url: "carddav.url", + username: "carddav.username", +}; + +const SYNCED_LDAP_PROPERTIES = { + protocolVersion: "protocolVersion", + authSASLMechanism: "auth.saslmech", + authDN: "auth.dn", + uri: "uri", + maxHits: "maxHits", +}; + +/** + * AddressBookRecord represents the state of an add-on in an application. + * + * Each add-on has its own record for each application ID it is installed + * on. + * + * The ID of add-on records is a randomly-generated GUID. It is random instead + * of deterministic so the URIs of the records cannot be guessed and so + * compromised server credentials won't result in disclosure of the specific + * add-ons present in a Sync account. + * + * The record contains the following fields: + * + */ +export function AddressBookRecord(collection, id) { + CryptoWrapper.call(this, collection, id); +} + +AddressBookRecord.prototype = { + __proto__: CryptoWrapper.prototype, + _logName: "Record.AddressBook", +}; +Utils.deferGetSet(AddressBookRecord, "cleartext", ["name", "type", "prefs"]); + +export function AddressBooksEngine(service) { + SyncEngine.call(this, "AddressBooks", service); +} + +AddressBooksEngine.prototype = { + __proto__: SyncEngine.prototype, + _storeObj: AddressBookStore, + _trackerObj: AddressBookTracker, + _recordObj: AddressBookRecord, + version: 1, + syncPriority: 6, + + /* + * Returns a changeset for this sync. Engine implementations can override this + * method to bypass the tracker for certain or all changed items. + */ + async getChangedIDs() { + return this._tracker.getChangedIDs(); + }, +}; + +function AddressBookStore(name, engine) { + Store.call(this, name, engine); +} +AddressBookStore.prototype = { + __proto__: Store.prototype, + + _addPrefsToBook(book, record, whichPrefs) { + for (let [key, realKey] of Object.entries(whichPrefs)) { + let value = record.prefs[key]; + let type = typeof value; + if (type == "string") { + book.setStringValue(realKey, value); + } else if (type == "number") { + book.setIntValue(realKey, value); + } else if (type == "boolean") { + book.setBoolValue(realKey, value); + } + } + }, + + /** + * Create an item in the store from a record. + * + * This is called by the default implementation of applyIncoming(). If using + * applyIncomingBatch(), this won't be called unless your store calls it. + * + * @param record + * The store record to create an item from + */ + async create(record) { + if ( + ![ + MailServices.ab.LDAP_DIRECTORY_TYPE, + MailServices.ab.CARDDAV_DIRECTORY_TYPE, + ].includes(record.type) + ) { + return; + } + + let dirPrefId = MailServices.ab.newAddressBook( + record.name, + null, + record.type, + record.id + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + + this._addPrefsToBook(book, record, SYNCED_COMMON_PROPERTIES); + if (record.type == MailServices.ab.CARDDAV_DIRECTORY_TYPE) { + this._addPrefsToBook(book, record, SYNCED_CARDDAV_PROPERTIES); + book.wrappedJSObject.fetchAllFromServer(); + } else if (record.type == MailServices.ab.LDAP_DIRECTORY_TYPE) { + this._addPrefsToBook(book, record, SYNCED_LDAP_PROPERTIES); + } + }, + + /** + * Remove an item in the store from a record. + * + * This is called by the default implementation of applyIncoming(). If using + * applyIncomingBatch(), this won't be called unless your store calls it. + * + * @param record + * The store record to delete an item from + */ + async remove(record) { + let book = MailServices.ab.getDirectoryFromUID(record.id); + if (!book) { + this._log.trace("Asked to remove record that doesn't exist, ignoring"); + return; + } + + let deletedPromise = new Promise(resolve => { + Services.obs.addObserver( + { + observe() { + Services.obs.removeObserver(this, "addrbook-directory-deleted"); + resolve(); + }, + }, + "addrbook-directory-deleted" + ); + }); + MailServices.ab.deleteAddressBook(book.URI); + await deletedPromise; + }, + + /** + * Update an item from a record. + * + * This is called by the default implementation of applyIncoming(). If using + * applyIncomingBatch(), this won't be called unless your store calls it. + * + * @param record + * The record to use to update an item from + */ + async update(record) { + let book = MailServices.ab.getDirectoryFromUID(record.id); + if (!book) { + this._log.trace("Skipping update for unknown item: " + record.id); + return; + } + if (book.dirType != record.type) { + throw new Components.Exception( + `Refusing to change book type from ${book.dirType} to ${record.type}`, + Cr.NS_ERROR_FAILURE + ); + } + + if (book.dirName != record.name) { + book.dirName = record.name; + } + this._addPrefsToBook(book, record, SYNCED_COMMON_PROPERTIES); + if (record.type == MailServices.ab.CARDDAV_DIRECTORY_TYPE) { + this._addPrefsToBook(book, record, SYNCED_CARDDAV_PROPERTIES); + } else if (record.type == MailServices.ab.LDAP_DIRECTORY_TYPE) { + this._addPrefsToBook(book, record, SYNCED_LDAP_PROPERTIES); + } + }, + + /** + * Determine whether a record with the specified ID exists. + * + * Takes a string record ID and returns a booleans saying whether the record + * exists. + * + * @param id + * string record ID + * @return boolean indicating whether record exists locally + */ + async itemExists(id) { + return id in (await this.getAllIDs()); + }, + + /** + * Obtain the set of all known record IDs. + * + * @return Object with ID strings as keys and values of true. The values + * are ignored. + */ + async getAllIDs() { + let ids = {}; + for (let b of MailServices.ab.directories) { + if ( + [ + MailServices.ab.LDAP_DIRECTORY_TYPE, + MailServices.ab.CARDDAV_DIRECTORY_TYPE, + ].includes(b.dirType) + ) { + ids[b.UID] = true; + } + } + return ids; + }, + + /** + * Create a record from the specified ID. + * + * If the ID is known, the record should be populated with metadata from + * the store. If the ID is not known, the record should be created with the + * delete field set to true. + * + * @param id + * string record ID + * @param collection + * Collection to add record to. This is typically passed into the + * constructor for the newly-created record. + * @return record type for this engine + */ + async createRecord(id, collection) { + let record = new AddressBookRecord(collection, id); + + let book = MailServices.ab.getDirectoryFromUID(id); + + // If we don't know about this ID, mark the record as deleted. + if (!book) { + record.deleted = true; + return record; + } + + record.name = book.dirName; + record.type = book.dirType; + record.prefs = {}; + + function collectPrefs(prefData) { + for (let [key, realKey] of Object.entries(prefData)) { + realKey = `${book.dirPrefId}.${realKey}`; + switch (Services.prefs.getPrefType(realKey)) { + case Services.prefs.PREF_STRING: + record.prefs[key] = Services.prefs.getStringPref(realKey); + break; + case Services.prefs.PREF_INT: + record.prefs[key] = Services.prefs.getIntPref(realKey); + break; + case Services.prefs.PREF_BOOL: + record.prefs[key] = Services.prefs.getBoolPref(realKey); + break; + } + } + } + + collectPrefs(SYNCED_COMMON_PROPERTIES); + + if (book.dirType == MailServices.ab.CARDDAV_DIRECTORY_TYPE) { + collectPrefs(SYNCED_CARDDAV_PROPERTIES); + } else if (book.dirType == MailServices.ab.LDAP_DIRECTORY_TYPE) { + collectPrefs(SYNCED_LDAP_PROPERTIES); + } + + return record; + }, +}; + +function AddressBookTracker(name, engine) { + Tracker.call(this, name, engine); +} +AddressBookTracker.prototype = { + __proto__: Tracker.prototype, + + _changedIDs: new Set(), + _ignoreAll: false, + + async getChangedIDs() { + let changes = {}; + for (let id of this._changedIDs) { + changes[id] = 0; + } + return changes; + }, + + clearChangedIDs() { + this._changedIDs.clear(); + }, + + get ignoreAll() { + return this._ignoreAll; + }, + + set ignoreAll(value) { + this._ignoreAll = value; + }, + + onStart() { + Services.prefs.addObserver("ldap_2.servers.", this); + Services.obs.addObserver(this, "addrbook-directory-created"); + Services.obs.addObserver(this, "addrbook-directory-deleted"); + }, + + onStop() { + Services.prefs.removeObserver("ldap_2.servers.", this); + Services.obs.removeObserver(this, "addrbook-directory-created"); + Services.obs.removeObserver(this, "addrbook-directory-deleted"); + }, + + observe(subject, topic, data) { + if (this._ignoreAll) { + return; + } + + let book; + switch (topic) { + case "nsPref:changed": { + let serverKey = data.split(".")[2]; + let prefName = data.substring(serverKey.length + 16); + if ( + prefName != "description" && + !Object.values(SYNCED_COMMON_PROPERTIES).includes(prefName) && + !Object.values(SYNCED_CARDDAV_PROPERTIES).includes(prefName) && + !Object.values(SYNCED_LDAP_PROPERTIES).includes(prefName) + ) { + return; + } + + book = MailServices.ab.getDirectoryFromId( + "ldap_2.servers." + serverKey + ); + break; + } + case "addrbook-directory-created": + case "addrbook-directory-deleted": + book = subject; + break; + } + + if ( + book && + [ + MailServices.ab.LDAP_DIRECTORY_TYPE, + MailServices.ab.CARDDAV_DIRECTORY_TYPE, + ].includes(book.dirType) && + !this._changedIDs.has(book.UID) + ) { + this._changedIDs.add(book.UID); + this.score += SCORE_INCREMENT_XLARGE; + } + }, +}; |