diff options
Diffstat (limited to '')
-rw-r--r-- | content/modules/addressbook.js | 1149 | ||||
-rw-r--r-- | content/modules/core.js | 332 | ||||
-rw-r--r-- | content/modules/db.js | 460 | ||||
-rw-r--r-- | content/modules/eventlog.js | 153 | ||||
-rw-r--r-- | content/modules/io.js | 41 | ||||
-rw-r--r-- | content/modules/lightning.js | 774 | ||||
-rw-r--r-- | content/modules/manager.js | 392 | ||||
-rw-r--r-- | content/modules/messenger.js | 99 | ||||
-rw-r--r-- | content/modules/network.js | 114 | ||||
-rw-r--r-- | content/modules/passwordManager.js | 78 | ||||
-rw-r--r-- | content/modules/providers.js | 184 | ||||
-rw-r--r-- | content/modules/public.js | 757 | ||||
-rw-r--r-- | content/modules/tools.js | 80 |
13 files changed, 4613 insertions, 0 deletions
diff --git a/content/modules/addressbook.js b/content/modules/addressbook.js new file mode 100644 index 0000000..d239a95 --- /dev/null +++ b/content/modules/addressbook.js @@ -0,0 +1,1149 @@ +/* + * This file is part of TbSync. + * + * 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 { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + AddrBookCard: "resource:///modules/AddrBookCard.jsm" +}); + +var addressbook = { + + _notifications: [ + "addrbook-directory-updated", + "addrbook-directory-deleted", + "addrbook-contact-created", + "addrbook-contact-updated", + "addrbook-contact-deleted", + "addrbook-list-member-added", + "addrbook-list-member-removed", + "addrbook-list-deleted", + "addrbook-list-updated", + "addrbook-list-created" + ], + + load : async function () { + for (let topic of this._notifications) { + Services.obs.addObserver(this.addressbookObserver, topic); + } + }, + + unload : async function () { + for (let topic of this._notifications) { + Services.obs.removeObserver(this.addressbookObserver, topic); + } + }, + + getStringValue : function (ab, value, fallback) { + try { + return ab.getStringValue(value, fallback); + } catch (e) { + return fallback; + } + }, + + searchDirectory: function (uri, search) { + return new Promise((resolve, reject) => { + let listener = { + cards : [], + + onSearchFinished(aResult, aErrorMsg) { + resolve(this.cards); + }, + onSearchFoundCard(aCard) { + this.cards.push(aCard.QueryInterface(Components.interfaces.nsIAbCard)); + } + } + + let result = MailServices.ab.getDirectory(uri).search(search, "", listener); + }); + }, + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * AdvancedTargetData, an extended TargetData implementation, providers + // * can use this as their own TargetData by extending it and just + // * defining the extra methods + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + AdvancedTargetData : class { + constructor(folderData) { + this._folderData = folderData; + this._targetObj = null; + } + + + // Check, if the target exists and return true/false. + hasTarget() { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + return directory ? true : false; + } + + // Returns the target obj, which TbSync should return as the target. It can + // be whatever you want and is returned by FolderData.targetData.getTarget(). + // If the target does not exist, it should be created. Throw a simple Error, if that + // failed. + async getTarget() { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + + if (!directory) { + // create a new addressbook and store its UID in folderData + directory = await TbSync.addressbook.prepareAndCreateAddressbook(this._folderData); + if (!directory) + throw new Error("notargets"); + } + + if (!this._targetObj || this._targetObj.UID != directory.UID) + this._targetObj = new TbSync.addressbook.AbDirectory(directory, this._folderData); + + return this._targetObj; + } + + /** + * Removes the target from the local storage. If it does not exist, return + * silently. A call to ``hasTarget()`` should return false, after this has + * been executed. + * + */ + removeTarget() { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + try { + if (directory) { + MailServices.ab.deleteAddressBook(directory.URI); + } + } catch (e) {} + + TbSync.db.clearChangeLog(target); + this._folderData.resetFolderProperty("target"); + } + + /** + * Disconnects the target in the local storage from this TargetData, but + * does not delete it, so it becomes a stale "left over" . A call + * to ``hasTarget()`` should return false, after this has been executed. + * + */ + disconnectTarget() { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + if (directory) { + let changes = TbSync.db.getItemsFromChangeLog(target, 0, "_by_user"); + if (changes.length > 0) { + this.targetName = this.targetName + " (*)"; + } + directory.setStringValue("tbSyncIcon", "orphaned"); + directory.setStringValue("tbSyncProvider", "orphaned"); + directory.setStringValue("tbSyncAccountID", ""); + } + TbSync.db.clearChangeLog(target); + this._folderData.resetFolderProperty("target"); + } + + set targetName(newName) { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + if (directory) { + directory.dirName = newName; + } else { + throw new Error("notargets"); + } + } + + get targetName() { + let target = this._folderData.getFolderProperty("target"); + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target); + if (directory) { + return directory.dirName; + } else { + throw new Error("notargets"); + } + } + + setReadOnly(value) { + } + + + // * * * * * * * * * * * * * * * * * + // * AdvancedTargetData extension * + // * * * * * * * * * * * * * * * * * + + get isAdvancedAddressbookTargetData() { + return true; + } + + get folderData() { + return this._folderData; + } + + // define a card property, which should be used for the changelog + // basically your primary key for the abItem properties + // UID will be used, if nothing specified + get primaryKeyField() { + return "UID"; + } + + generatePrimaryKey() { + return TbSync.generateUUID(); + } + + // enable or disable changelog + get logUserChanges() { + return true; + } + + directoryObserver(aTopic) { + switch (aTopic) { + case "addrbook-directory-deleted": + case "addrbook-directory-updated": + //Services.console.logStringMessage("["+ aTopic + "] " + folderData.getFolderProperty("foldername")); + break; + } + } + + cardObserver(aTopic, abCardItem) { + switch (aTopic) { + case "addrbook-contact-updated": + case "addrbook-contact-deleted": + case "addrbook-contact-created": + //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName")); + break; + } + } + + listObserver(aTopic, abListItem, abListMember) { + switch (aTopic) { + case "addrbook-list-member-added": + case "addrbook-list-member-removed": + //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName")); + break; + + case "addrbook-list-deleted": + case "addrbook-list-updated": + //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName")); + break; + + case "addrbook-list-created": + //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for List <"+abListItem.getProperty("ListName")+">"); + break; + } + } + + // replace this with your own implementation to create the actual addressbook, + // when this class is extended + async createAddressbook(newname) { + // https://searchfox.org/comm-central/source/mailnews/addrbook/src/nsDirPrefs.h + let dirPrefId = MailServices.ab.newAddressBook(newname, "", 101); + let directory = MailServices.ab.getDirectoryFromId(dirPrefId); + return directory; + } + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * AbItem and AbDirectory Classes + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + AbItem : class { + constructor(abDirectory, item) { + if (!abDirectory) + throw new Error("AbItem::constructor is missing its first parameter!"); + + if (!item) + throw new Error("AbItem::constructor is missing its second parameter!"); + + this._abDirectory = abDirectory; + this._card = null; + this._tempListDirectory = null; + this._tempProperties = null; + this._isMailList = false; + + if (item instanceof Components.interfaces.nsIAbDirectory) { + this._tempListDirectory = item; + this._isMailList = true; + this._tempProperties = {}; + } else { + this._card = item; + this._isMailList = item.isMailList; + } + } + + get abDirectory() { + return this._abDirectory; + } + + get isMailList() { + return this._isMailList; + } + + + + + + get nativeItem() { + return this._card; + } + + get UID() { + if (this._tempListDirectory) return this._tempListDirectory.UID; + return this._card.UID; + } + + get primaryKey() { + //use UID as fallback + let key = this._abDirectory.primaryKeyField; + return key ? this.getProperty(key) : this.UID; + } + + set primaryKey(value) { + //use UID as fallback + let key = this._abDirectory.primaryKeyField; + if (key) this.setProperty(key, value) + else throw ("TbSync.addressbook.AbItem.set primaryKey: UID is used as primaryKeyField but changing the UID of an item is currently not supported. Please use a custom primaryKeyField."); + } + + clone() { //no real clone ... this is just here to match the calendar target + return new TbSync.addressbook.AbItem(this._abDirectory, this._card); + } + + toString() { + return this._card.displayName + " (" + this._card.firstName + ", " + this._card.lastName + ") <"+this._card.primaryEmail+">"; + } + + // mailinglist aware method to get properties of cards + // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog + getProperty(property, fallback = "") { + if (property == "UID") + return this.UID; + + if (this._isMailList) { + const directListProperties = { + ListName: "dirName", + ListNickName: "listNickName", + ListDescription: "description" + }; + + let value; + if (directListProperties.hasOwnProperty(property)) { + try { + let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI); //this._card.asDirectory + value = mailListDirectory[directListProperties[property]]; + } catch (e) { + // list does not exists + } + } else { + value = this._tempProperties ? this._tempProperties[property] : TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID + "#" + this.UID, property); + } + return value || fallback; + } else { + return this._card.getProperty(property, fallback); + } + } + + // mailinglist aware method to set properties of cards + // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog + // while the list has not been added, we keep all props in an object (UID changes on adding) + setProperty(property, value) { + // UID cannot be changed (currently) + if (property == "UID") { + throw ("TbSync.addressbook.AbItem.setProperty: UID cannot be changed currently."); + return; + } + + if (this._isMailList) { + const directListProperties = { + ListName: "dirName", + ListNickName: "listNickName", + ListDescription: "description" + }; + + if (directListProperties.hasOwnProperty(property)) { + try { + let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI); + mailListDirectory[directListProperties[property]] = value; + } catch (e) { + // list does not exists + } + } else { + if (this._tempProperties) { + this._tempProperties[property] = value; + } else { + TbSync.db.addItemToChangeLog(this._abDirectory.UID + "#" + this.UID, property, value); + } + } + } else { + this._card.setProperty(property, value); + } + } + + deleteProperty(property) { + if (this._isMailList) { + if (this._tempProperties) { + delete this._tempProperties[property]; + } else { + TbSync.db.removeItemFromChangeLog(this._abDirectory.UID + "#" + this.UID, property); + } + } else { + this._card.deleteProperty(property); + } + } + + get changelogData() { + return TbSync.db.getItemDataFromChangeLog(this._abDirectory.UID, this.primaryKey); + } + + get changelogStatus() { + return TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID, this.primaryKey); + } + + set changelogStatus(status) { + let value = this.primaryKey; + + if (value) { + if (!status) { + TbSync.db.removeItemFromChangeLog(this._abDirectory.UID, value); + return; + } + + if (this._abDirectory.logUserChanges || status.endsWith("_by_server")) { + TbSync.db.addItemToChangeLog(this._abDirectory.UID, value, status); + } + } + } + + + + + + // get the property given from all members and return it as an array (that property better be uniqe) + getMembersPropertyList(property) { + let members = []; + if (this._card && this._card.isMailList) { + // get mailListDirectory + let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); + for (let member of mailListDirectory.childCards) { + let prop = member.getProperty(property, ""); + if (prop) members.push(prop); + } + } + return members; + } + + addListMembers(property, candidates) { + if (this._card && this._card.isMailList) { + let members = this.getMembersPropertyList(property); + let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); + + for (let candidate of candidates) { + if (members.includes(candidate)) + continue; + + let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true); + if (card) mailListDirectory.addCard(card); + } + } + } + + removeListMembers(property, candidates) { + if (this._card && this._card.isMailList) { + let members = this.getMembersPropertyList(property); + let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI); + + let cardsToRemove = []; + for (let candidate of candidates) { + if (!members.includes(candidate)) + continue; + + let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true); + if (card) cardsToRemove.push(card); + } + if (cardsToRemove.length > 0) mailListDirectory.deleteCards(cardsToRemove); + } + } + + addPhoto(photo, data, extension = "jpg", url = "") { + let dest = []; + let card = this._card; + let bookUID = this.abDirectory.UID; + + // TbSync storage must be set as last + let book64 = btoa(bookUID); + let photo64 = btoa(photo); + let photoName64 = book64 + "_" + photo64 + "." + extension; + + dest.push(["Photos", photoName64]); + // I no longer see a reason for this + // dest.push(["TbSync","Photos", book64, photo64]); + + let filePath = ""; + for (let i=0; i < dest.length; i++) { + let file = FileUtils.getFile("ProfD", dest[i]); + + let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream); + foStream.init(file, 0x02 | 0x08 | 0x20, 0x180, 0); // write, create, truncate + let binary = ""; + try { + binary = atob(data.split(" ").join("")); + } catch (e) { + console.log("Failed to decode base64 string:", data); + } + foStream.write(binary, binary.length); + foStream.close(); + + filePath = 'file:///' + file.path.replace(/\\/g, '\/').replace(/^\s*\/?/, '').replace(/\ /g, '%20'); + } + card.setProperty("PhotoName", photoName64); + card.setProperty("PhotoType", url ? "web" : "file"); + card.setProperty("PhotoURI", url ? url : filePath); + return filePath; + } + + getPhoto() { + let card = this._card; + let photo = card.getProperty("PhotoName", ""); + let data = ""; + + if (photo) { + try { + let file = FileUtils.getFile("ProfD", ["Photos", photo]); + + let fiStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream); + fiStream.init(file, -1, -1, false); + + let bstream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream); + bstream.setInputStream(fiStream); + + data = btoa(bstream.readBytes(bstream.available())); + fiStream.close(); + } catch (e) {} + } + return data; + } + }, + + AbDirectory : class { + constructor(directory, folderData) { + this._directory = directory; + this._folderData = folderData; + } + + get directory() { + return this._directory; + } + + get logUserChanges() { + return this._folderData.targetData.logUserChanges; + } + + get primaryKeyField() { + return this._folderData.targetData.primaryKeyField; + } + + get UID() { + return this._directory.UID; + } + + get URI() { + return this._directory.URI; + } + + createNewCard() { + let card = new AddrBookCard(); + return new TbSync.addressbook.AbItem(this, card); + } + + createNewList() { + let listDirectory = Components.classes["@mozilla.org/addressbook/directoryproperty;1"].createInstance(Components.interfaces.nsIAbDirectory); + listDirectory.isMailList = true; + return new TbSync.addressbook.AbItem(this, listDirectory); + } + + async addItem(abItem, pretagChangelogWithByServerEntry = true) { + if (this.primaryKeyField && !abItem.getProperty(this.primaryKeyField)) { + abItem.setProperty(this.primaryKeyField, this._folderData.targetData.generatePrimaryKey()); + //Services.console.logStringMessage("[AbDirectory::addItem] Generated primary key!"); + } + + if (pretagChangelogWithByServerEntry) { + abItem.changelogStatus = "added_by_server"; + } + + if (abItem.isMailList && abItem._tempListDirectory) { + let list = this._directory.addMailList(abItem._tempListDirectory); + // the list has been added and we can now get the corresponding card via its UID + let found = await this.getItemFromProperty("UID", list.UID); + + // clone and clear temporary properties + let props = {...abItem._tempProperties}; + abItem._tempListDirectory = null; + abItem._tempProperties = null; + + // store temporary properties + for (const [property, value] of Object.entries(props)) { + found.setProperty(property, value); + } + + abItem._card = found._card; + } else if (!abItem.isMailList) { + this._directory.addCard(abItem._card); + + } else { + throw new Error("Cannot re-add a list to a directory."); + } + } + + modifyItem(abItem, pretagChangelogWithByServerEntry = true) { + // only add entry if the current entry does not start with _by_user + let status = abItem.changelogStatus ? abItem.changelogStatus : ""; + if (pretagChangelogWithByServerEntry && !status.endsWith("_by_user")) { + abItem.changelogStatus = "modified_by_server"; + } + + if (abItem.isMailList) { + // get mailListDirectory + let mailListDirectory = MailServices.ab.getDirectory(abItem._card.mailListURI); + + // store + mailListDirectory.editMailListToDatabase(abItem._card); + } else { + this._directory.modifyCard(abItem._card); + } + } + + deleteItem(abItem, pretagChangelogWithByServerEntry = true) { + if (pretagChangelogWithByServerEntry) { + abItem.changelogStatus = "deleted_by_server"; + } + this._directory.deleteCards([abItem._card]); + } + + async getItem(searchId) { + //use UID as fallback + let key = this.primaryKeyField ? this.primaryKeyField : "UID"; + return await this.getItemFromProperty(key, searchId); + } + + async getItemFromProperty(property, value) { + // try to use the standard card method first + let card = this._directory.getCardFromProperty(property, value, true); + if (card) { + return new TbSync.addressbook.AbItem(this, card); + } + + // search for list cards + // we cannot search for the prop directly, because for mailinglists + // they are not part of the card (expect UID) but stored in a custom storage + let searchList = "(IsMailList,=,TRUE)"; + let foundCards = await TbSync.addressbook.searchDirectory(this._directory.URI, "(or" + searchList+")"); + for (let aCard of foundCards) { + let card = new TbSync.addressbook.AbItem(this, aCard); + //does this list card have the req prop? + if (card.getProperty(property) == value) { + return card; + } + } + return null; + } + + getAllItems () { + let rv = []; + for (let card of this._directory.childCards) { + rv.push(new TbSync.addressbook.AbItem( this._directory, card )); + } + return rv; + } + + + + + + getAddedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "added_by_user").map(item => item.itemId); + } + + getModifiedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "modified_by_user").map(item => item.itemId); + } + + getDeletedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "deleted_by_user").map(item => item.itemId); + } + + getItemsFromChangeLog(maxitems = 0) { // Document what this returns + return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "_by_user"); + } + + removeItemFromChangeLog(id, moveToEndInsteadOfDelete = false) { + TbSync.db.removeItemFromChangeLog(this._directory.UID, id, moveToEndInsteadOfDelete); + } + + clearChangelog() { + TbSync.db.clearChangeLog(this._directory.UID); + } + + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * Internal Functions + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + prepareAndCreateAddressbook: async function (folderData) { + let target = folderData.getFolderProperty("target"); + let provider = folderData.accountData.getAccountProperty("provider"); + + // Get cached or new unique name for new address book + let cachedName = folderData.getFolderProperty("targetName"); + let newname = cachedName == "" ? folderData.accountData.getAccountProperty("accountname") + " (" + folderData.getFolderProperty("foldername")+ ")" : cachedName; + + //Create the new book with the unique name + let directory = await folderData.targetData.createAddressbook(newname); + if (directory && directory instanceof Components.interfaces.nsIAbDirectory) { + directory.setStringValue("tbSyncProvider", provider); + directory.setStringValue("tbSyncAccountID", folderData.accountData.accountID); + + // Prevent gContactSync to inject its stuff into New/EditCard dialogs + // https://github.com/jdgeenen/gcontactsync/pull/127 + directory.setStringValue("gContactSyncSkipped", "true"); + + folderData.setFolderProperty("target", directory.UID); + folderData.setFolderProperty("targetName", directory.dirName); + //notify about new created address book + Services.obs.notifyObservers(null, 'tbsync.observer.addressbook.created', null) + return directory; + } + + return null; + }, + + getFolderFromDirectoryUID: function(bookUID) { + let folders = TbSync.db.findFolders({"target": bookUID}); + if (folders.length == 1) { + let accountData = new TbSync.AccountData(folders[0].accountID); + return new TbSync.FolderData(accountData, folders[0].folderID); + } + return null; + }, + + getDirectoryFromDirectoryUID: function(UID) { + if (!UID) + return null; + + for (let directory of MailServices.ab.directories) { + if (directory instanceof Components.interfaces.nsIAbDirectory) { + if (directory.UID == UID) return directory; + } + } + return null; + }, + + getListInfoFromListUID: async function(UID) { + for (let directory of MailServices.ab.directories) { + if (directory instanceof Components.interfaces.nsIAbDirectory && !directory.isRemote) { + let searchList = "(IsMailList,=,TRUE)"; + let foundCards = await TbSync.addressbook.searchDirectory(directory.URI, "(and" + searchList+")"); + for (let listCard of foundCards) { + //return after first found card + if (listCard.UID == UID) return {directory, listCard}; + } + } + } + throw new Error("List with UID <" + UID + "> does not exists"); + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * Addressbook Observer and Listener + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + addressbookObserver: { + observe: async function (aSubject, aTopic, aData) { + switch (aTopic) { + // we do not need addrbook-created + case "addrbook-directory-updated": + case "addrbook-directory-deleted": + { + //aSubject: nsIAbDirectory (we can get URI and UID directly from the object, but the directory no longer exists) + aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); + let bookUID = aSubject.UID; + + let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedAddressbookTargetData) { + + switch(aTopic) { + case "addrbook-directory-updated": + { + //update name of target (if changed) + folderData.setFolderProperty("targetName", aSubject.dirName); + //update settings window, if open + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + } + break; + + case "addrbook-directory-deleted": + { + //delete any pending changelog of the deleted book + TbSync.db.clearChangeLog(bookUID); + + //unselect book if deleted by user and update settings window, if open + if (folderData.getFolderProperty("selected")) { + folderData.setFolderProperty("selected", false); + //update settings window, if open + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + } + + folderData.resetFolderProperty("target"); + } + break; + } + + folderData.targetData.directoryObserver(aTopic); + } + } + break; + + case "addrbook-contact-created": + case "addrbook-contact-updated": + case "addrbook-contact-deleted": + { + //aSubject: nsIAbCard + aSubject.QueryInterface(Components.interfaces.nsIAbCard); + //aData: 128-bit unique identifier for the parent directory + let bookUID = aData; + + let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedAddressbookTargetData) { + + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID); + let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData); + let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject); + let itemStatus = abItem.changelogStatus || ""; + + // during create the following can happen + // card has no primary key + // another process could try to mod + // -> we need to identify this card with an always available ID and block any other MODS until we free it again + // -> store creation type + + if (aTopic == "addrbook-contact-created" && itemStatus == "") { + // add this new card to changelog to keep track of it + TbSync.db.addItemToChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation", Date.now()); + // new cards must get a NEW(!) primaryKey first + if (abDirectory.primaryKeyField) { + console.log("New primary Key generated!"); + abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey()); + } + // special case: do not add "modified_by_server" + abDirectory.modifyItem(abItem, /*pretagChangelogWithByServerEntry */ false); + // We will see this card again as updated but delayed created + return; + } + + // during follow up MODs we can identify this card via + let delayedUserCreation = TbSync.db.getItemStatusFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation"); + + // if we reach this point and if we have adelayedUserCreation, + // we can remove the delayedUserCreation marker and can + // continue to process this event as an addrbook-contact-created + let bTopic = aTopic; + if (delayedUserCreation) { + let age = Date.now() - delayedUserCreation; + if (age < 1500) { + bTopic = "addrbook-contact-created"; + } else { + TbSync.db.removeItemFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation"); + } + } + + // if this card was created by us, it will be in the log + // we want to ignore any MOD for a freeze time, because + // gContactSync modifies our(!) contacts (GoogleID) after we added them, so they get + // turned into "modified_by_user" and will be send back to the server. + if (itemStatus && itemStatus.endsWith("_by_server")) { + let age = Date.now() - abItem.changelogData.timestamp; + if (age < 1500) { + // during freeze, local modifications are not possible + return; + } else { + // remove blocking entry from changelog after freeze time is over (1.5s), + // and continue evaluating this event + abItem.changelogStatus = ""; + } + } + + // From here on, we only process user changes as server changes are self freezed + // update changelog based on old status + switch (bTopic) { + case "addrbook-contact-created": + { + switch (itemStatus) { + case "added_by_user": + // late create notification + break; + + case "modified_by_user": + // late create notification + abItem.changelogStatus = "added_by_user"; + break; + + case "deleted_by_user": + // unprocessed delete for this card, undo the delete (moved out and back in) + abItem.changelogStatus = "modified_by_user"; + break; + + default: + // new card + abItem.changelogStatus = "added_by_user"; + } + } + break; + + case "addrbook-contact-updated": + { + switch (itemStatus) { + case "added_by_user": + // unprocessed add for this card, keep status + break; + + case "modified_by_user": + // double notification, keep status + break; + + case "deleted_by_user": + // race? unprocessed delete for this card, moved out and back in and modified + default: + abItem.changelogStatus = "modified_by_user"; + break; + } + } + break; + + case "addrbook-contact-deleted": + { + switch (itemStatus) { + case "added_by_user": + // unprocessed add for this card, revert + abItem.changelogStatus = ""; + return; + + case "deleted_by_user": + // double notification + break; + + case "modified_by_user": + // unprocessed mod for this card + default: + abItem.changelogStatus = "deleted_by_user"; + break; + } + } + break; + } + + if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); + + // notify observers only if status changed + if (itemStatus != abItem.changelogStatus) { + folderData.targetData.cardObserver(bTopic, abItem); + } + return; + } + } + break; + + case "addrbook-list-created": + case "addrbook-list-deleted": + { + //aSubject: nsIAbDirectory + aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); + //aData: 128-bit unique identifier for the parent directory + let bookUID = aData; + + let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedAddressbookTargetData) { + + let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID); + let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData); + let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject); + + let itemStatus = abItem.changelogStatus; + if (itemStatus && itemStatus.endsWith("_by_server")) { + //we caused this, ignore + abItem.changelogStatus = ""; + return; + } + + // update changelog based on old status + switch (aTopic) { + case "addrbook-list-created": + { + if (abDirectory.primaryKeyField) { + // Since we do not need to update a list, to make custom properties persistent, we do not need to use delayedUserCreation as with contacts. + abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey()); + } + + switch (itemStatus) { + case "added_by_user": + // double notification, which is probably impossible, keep status + break; + + case "modified_by_user": + // late create notification + abItem.changelogStatus = "added_by_user"; + break; + + case "deleted_by_user": + // unprocessed delete for this card, undo the delete (moved out and back in) + abItem.changelogStatus = "modified_by_user"; + break; + + default: + // new list + abItem.changelogStatus = "added_by_user"; + break; + } + } + break; + + case "addrbook-list-deleted": + { + switch (itemStatus) { + case "added_by_user": + // unprocessed add for this card, revert + abItem.changelogStatus = ""; + return; + + case "modified_by_user": + // unprocessed mod for this card + case "deleted_by_user": + // double notification + default: + abItem.changelogStatus = "deleted_by_user"; + break; + } + //remove properties of this ML stored in changelog + TbSync.db.clearChangeLog(abDirectory.UID + "#" + abItem.UID); + } + break; + } + + if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.listObserver(aTopic, abItem, null); + } + } + break; + + case "addrbook-list-updated": + { + // aSubject: nsIAbDirectory + aSubject.QueryInterface(Components.interfaces.nsIAbDirectory); + // get the card representation of this list, including its parent directory + let listInfo = await TbSync.addressbook.getListInfoFromListUID(aSubject.UID); + let bookUID = listInfo.directory.UID; + + let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedAddressbookTargetData) { + + let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData); + let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard); + + let itemStatus = abItem.changelogStatus; + if (itemStatus && itemStatus.endsWith("_by_server")) { + //we caused this, ignore + abItem.changelogStatus = ""; + return; + } + + // update changelog based on old status + switch (aTopic) { + case "addrbook-list-updated": + { + switch (itemStatus) { + case "added_by_user": + // unprocessed add for this card, keep status + break; + + case "modified_by_user": + // double notification, keep status + break; + + case "deleted_by_user": + // race? unprocessed delete for this card, moved out and back in and modified + default: + abItem.changelogStatus = "modified_by_user"; + break; + } + } + break; + } + + if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.listObserver(aTopic, abItem, null); + } + } + break; + + // unknown, if called for programmatically added members as well, probably not + case "addrbook-list-member-added": //exclude contact without Email - notification is wrongly send + case "addrbook-list-member-removed": + { + //aSubject: nsIAbCard of Member + aSubject.QueryInterface(Components.interfaces.nsIAbCard); + //aData: 128-bit unique identifier for the list + let listInfo = await TbSync.addressbook.getListInfoFromListUID(aData); + let bookUID = listInfo.directory.UID; + + let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedAddressbookTargetData) { + + let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData); + let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard); + let abMember = new TbSync.addressbook.AbItem(abDirectory, aSubject); + + if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.listObserver(aTopic, abItem, abMember); + + // removed, added members cause the list to be changed + let mailListDirectory = MailServices.ab.getDirectory(listInfo.listCard.mailListURI); + TbSync.addressbook.addressbookObserver.observe(mailListDirectory, "addrbook-list-updated", null); + return; + } + } + break; + + } + } + }, + +} diff --git a/content/modules/core.js b/content/modules/core.js new file mode 100644 index 0000000..6a82af3 --- /dev/null +++ b/content/modules/core.js @@ -0,0 +1,332 @@ +/* + * This file is part of TbSync. + * + * 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 core = { + + syncDataObj : null, + + load: async function () { + this.syncDataObj = {}; + }, + + unload: async function () { + }, + + isSyncing: function (accountID) { + let status = TbSync.db.getAccountProperty(accountID, "status"); //global status of the account + return (status == "syncing"); + }, + + isEnabled: function (accountID) { + let status = TbSync.db.getAccountProperty(accountID, "status"); + return (status != "disabled"); + }, + + isConnected: function (accountID) { + let status = TbSync.db.getAccountProperty(accountID, "status"); + let validFolders = TbSync.db.findFolders({"cached": false}, {"accountID": accountID}); + return (status != "disabled" && validFolders.length > 0); + }, + + resetSyncDataObj: function (accountID) { + this.syncDataObj[accountID] = new TbSync.SyncData(accountID); + }, + + getSyncDataObject: function (accountID) { + if (!this.syncDataObj.hasOwnProperty(accountID)) { + this.resetSyncDataObj(accountID); + } + return this.syncDataObj[accountID]; + }, + + getNextPendingFolder: function (syncData) { + let sortedFolders = TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.getSortedFolders(syncData.accountData); + for (let i=0; i < sortedFolders.length; i++) { + if (sortedFolders[i].getFolderProperty("status") != "pending") continue; + syncData._setCurrentFolderData(sortedFolders[i]); + return true; + } + syncData._clearCurrentFolderData(); + return false; + }, + + + syncAllAccounts: function () { + //get info of all accounts + let accounts = TbSync.db.getAccounts(); + + for (let i=0; i < accounts.IDs.length; i++) { + // core async sync function, but we do not wait until it has finished, + // but return right away and initiate sync of all accounts parallel + this.syncAccount(accounts.IDs[i]); + } + }, + + syncAccount: async function (accountID, aSyncDescription = {}) { + let syncDescription = {}; + Object.assign(syncDescription, aSyncDescription); + + if (!syncDescription.hasOwnProperty("maxAccountReruns")) syncDescription.maxAccountReruns = 2; + if (!syncDescription.hasOwnProperty("maxFolderReruns")) syncDescription.maxFolderReruns = 2; + if (!syncDescription.hasOwnProperty("syncList")) syncDescription.syncList = true; + if (!syncDescription.hasOwnProperty("syncFolders")) syncDescription.syncFolders = null; // null ( = default = sync selected folders) or (empty) Array with folderData obj to be synced + if (!syncDescription.hasOwnProperty("syncJob")) syncDescription.syncJob = "sync"; + + //do not init sync if there is a sync running or account is not enabled + if (!this.isEnabled(accountID) || this.isSyncing(accountID)) return; + + //create syncData object for each account (to be able to have parallel XHR) + this.resetSyncDataObj(accountID); + let syncData = this.getSyncDataObject(accountID); + + //send GUI into lock mode (status == syncing) + TbSync.db.setAccountProperty(accountID, "status", "syncing"); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountSettingsGui", accountID); + + let overallStatusData = new TbSync.StatusData(); + let accountRerun; + let accountRuns = 0; + + do { + accountRerun = false; + + if (accountRuns > syncDescription.maxAccountReruns) { + overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "resync-loop"); + break; + } + accountRuns++; + + if (syncDescription.syncList) { + let listStatusData; + try { + listStatusData = await TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.syncFolderList(syncData, syncDescription.syncJob, accountRuns); + } catch (e) { + listStatusData = new TbSync.StatusData(TbSync.StatusData.WARNING, "JavaScriptError", e.message + "\n\n" + e.stack); + } + + if (!(listStatusData instanceof TbSync.StatusData)) { + overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "apiError", "TbSync/"+syncData.accountData.getAccountProperty("provider")+": Base.syncFolderList() must return a StatusData object"); + break; + } + + //if we have an error during folderList sync, there is no need to go on + if (listStatusData.type != TbSync.StatusData.SUCCESS) { + overallStatusData = listStatusData; + accountRerun = (listStatusData.type == TbSync.StatusData.ACCOUNT_RERUN) + TbSync.eventlog.add(listStatusData.type, syncData.eventLogInfo, listStatusData.message, listStatusData.details); + continue; //jumps to the while condition check + } + + // Removes all leftover cached folders and sets all other folders to a well defined cached = "0" + // which will set this account as connected (if at least one non-cached folder is present). + this.removeCachedFolders(syncData); + + // update folder list in GUI + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID); + } + + // syncDescription.syncFolders is either null ( = default = sync selected folders) or an Array. + // Skip folder sync if Array is empty. + if (!Array.isArray(syncDescription.syncFolders) || syncDescription.syncFolders.length > 0) { + this.prepareFoldersForSync(syncData, syncDescription); + + // update folder list in GUI + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID); + + // if any folder was found, sync + if (syncData.accountData.isConnected()) { + let folderRuns = 1; + do { + if (folderRuns > syncDescription.maxFolderReruns) { + overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "resync-loop"); + break; + } + + // getNextPendingFolder will set or clear currentFolderData of syncData + if (!this.getNextPendingFolder(syncData)) { + break; + } + + let folderStatusData; + try { + folderStatusData = await TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.syncFolder(syncData, syncDescription.syncJob, folderRuns); + } catch (e) { + folderStatusData = new TbSync.StatusData(TbSync.StatusData.WARNING, "JavaScriptError", e.message + "\n\n" + e.stack); + } + + if (!(folderStatusData instanceof TbSync.StatusData)) { + folderStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "apiError", "TbSync/"+syncData.accountData.getAccountProperty("provider")+": Base.syncFolder() must return a StatusData object"); + } + + // if one of the folders indicated a FOLDER_RERUN, do not finish this + // folder but do it again + if (folderStatusData.type == TbSync.StatusData.FOLDER_RERUN) { + TbSync.eventlog.add(folderStatusData.type, syncData.eventLogInfo, folderStatusData.message, folderStatusData.details); + folderRuns++; + continue; + } else { + folderRuns = 1; + } + + this.finishFolderSync(syncData, folderStatusData); + + //if one of the folders indicated an ERROR, abort sync + if (folderStatusData.type == TbSync.StatusData.ERROR) { + break; + } + + //if the folder has send an ACCOUNT_RERUN, abort sync and rerun the entire account + if (folderStatusData.type == TbSync.StatusData.ACCOUNT_RERUN) { + syncDescription.syncList = true; + accountRerun = true; + break; + } + + } while (true); + } else { + overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "no-folders-found-on-server"); + } + } + + } while (accountRerun); + + this.finishAccountSync(syncData, overallStatusData); + }, + + // this could be added to AccountData, but I do not want that in public + setTargetModified: function (folderData) { + if (!folderData.accountData.isSyncing() && folderData.accountData.isEnabled()) { + folderData.accountData.setAccountProperty("status", "notsyncronized"); + folderData.setFolderProperty("status", "modified"); + //notify settings gui to update status + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + } + }, + + enableAccount: function(accountID) { + let accountData = new TbSync.AccountData(accountID); + TbSync.providers[accountData.getAccountProperty("provider")].Base.onEnableAccount(accountData); + accountData.setAccountProperty("status", "notsyncronized"); + accountData.resetAccountProperty("lastsynctime"); + }, + + disableAccount: function(accountID) { + let accountData = new TbSync.AccountData(accountID); + TbSync.providers[accountData.getAccountProperty("provider")].Base.onDisableAccount(accountData); + accountData.setAccountProperty("status", "disabled"); + + let folders = accountData.getAllFolders(); + for (let folder of folders) { + if (folder.getFolderProperty("selected")) { + folder.targetData.removeTarget(); + folder.setFolderProperty("selected", false); + } + folder.setFolderProperty("cached", true); + } + }, + + //removes all leftover cached folders and sets all other folders to a well defined cached = "0" + //which will set this account as connected (if at least one non-cached folder is present) + removeCachedFolders: function(syncData) { + let folders = syncData.accountData.getAllFoldersIncludingCache(); + for (let folder of folders) { + //delete all leftover cached folders + if (folder.getFolderProperty("cached")) { + TbSync.db.deleteFolder(folder.accountID, folder.folderID); + continue; + } else { + //set well defined cache state + folder.setFolderProperty("cached", false); + } + } + }, + + //set allrequested folders to "pending", so they are marked for syncing + prepareFoldersForSync: function(syncData, syncDescription) { + let folders = syncData.accountData.getAllFolders(); + for (let folder of folders) { + let requested = (Array.isArray(syncDescription.syncFolders) && syncDescription.syncFolders.filter(f => f.folderID == folder.folderID).length > 0); + let selected = (!Array.isArray(syncDescription.syncFolders) && folder.getFolderProperty("selected")); + + //set folders to pending, so they get synced + if (requested || selected) { + folder.setFolderProperty("status", "pending"); + } + } + }, + + finishFolderSync: function(syncData, statusData) { + if (statusData.type != TbSync.StatusData.SUCCESS) { + //report error + TbSync.eventlog.add(statusData.type, syncData.eventLogInfo, statusData.message, statusData.details); + } + + //if this is a success, prepend success to the status message, + //otherwise just set the message + let status; + if (statusData.type == TbSync.StatusData.SUCCESS || statusData.message == "") { + status = statusData.type; + if (statusData.message) status = status + "." + statusData.message; + } else { + status = statusData.message; + } + + if (syncData.currentFolderData) { + syncData.currentFolderData.setFolderProperty("status", status); + syncData.currentFolderData.setFolderProperty("lastsynctime", Date.now()); + //clear folderID to fall back to account-only-mode (folder is done!) + syncData._clearCurrentFolderData(); + } + + syncData.setSyncState("done"); + }, + + finishAccountSync: function(syncData, statusData) { + // set each folder with PENDING status to ABORTED + let folders = TbSync.db.findFolders({"status": "pending"}, {"accountID": syncData.accountData.accountID}); + for (let i=0; i < folders.length; i++) { + TbSync.db.setFolderProperty(folders[i].accountID, folders[i].folderID, "status", "aborted"); + } + + //if this is a success, prepend success to the status message, + //otherwise just set the message + let status; + if (statusData.type == TbSync.StatusData.SUCCESS || statusData.message == "") { + status = statusData.type; + if (statusData.message) status = status + "." + statusData.message; + } else { + status = statusData.message; + } + + + if (statusData.type != TbSync.StatusData.SUCCESS) { + //report error + TbSync.eventlog.add("warning", syncData.eventLogInfo, statusData.message, statusData.details); + } else { + //account itself is ok, search for folders with error + folders = TbSync.db.findFolders({"selected": true, "cached": false}, {"accountID": syncData.accountData.accountID}); + for (let i in folders) { + let folderstatus = folders[i].data.status.split(".")[0]; + if (folderstatus != "" && folderstatus != TbSync.StatusData.SUCCESS && folderstatus != "aborted") { + status = "foldererror"; + break; + } + } + } + + //done + syncData.accountData.setAccountProperty("lastsynctime", Date.now()); + syncData.accountData.setAccountProperty("status", status); + syncData.setSyncState("accountdone"); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID); + this.resetSyncDataObj(syncData.accountData.accountID); + } + +} diff --git a/content/modules/db.js b/content/modules/db.js new file mode 100644 index 0000000..730f19a --- /dev/null +++ b/content/modules/db.js @@ -0,0 +1,460 @@ +/* + * This file is part of TbSync. + * + * 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 { DeferredTask } = ChromeUtils.import("resource://gre/modules/DeferredTask.jsm"); + +var db = { + + loaded: false, + + files: { + accounts: { + name: "accounts68.json", + default: JSON.stringify({ sequence: 0, data : {} }) + //data[account] = {row} + }, + folders: { + name: "folders68.json", + default: JSON.stringify({}) + //assoziative array of assoziative array : folders[<int>accountID][<string>folderID] = {row} + }, + changelog: { + name: "changelog68.json", + default: JSON.stringify([]), + }, + }, + + load: async function () { + //DB Concept: + //-- on application start, data is read async from json file into object + //-- add-on only works on object + //-- each time data is changed, an async write job is initiated <writeDelay>ms in the future and is resceduled, if another request arrives within that time + + for (let f in this.files) { + this.files[f].write = new DeferredTask(() => this.writeAsync(f), 6000); + + try { + this[f] = await IOUtils.readJSON(TbSync.io.getAbsolutePath(this.files[f].name)); + this.files[f].found = true; + } catch (e) { + //if there is no file, there is no file... + this[f] = JSON.parse(this.files[f].default); + this.files[f].found = false; + Components.utils.reportError(e); + } + } + + function getNewDeviceId4Migration() { + //taken from https://jsfiddle.net/briguy37/2MVFd/ + let d = new Date().getTime(); + let uuid = 'xxxxxxxxxxxxxxxxyxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + let r = (d + Math.random()*16)%16 | 0; + d = Math.floor(d/16); + return (c=='x' ? r : (r&0x3|0x8)).toString(16); + }); + return "MZTB" + uuid; + } + + // try to migrate old accounts file from TB60 + if (!this.files["accounts"].found) { + try { + let accounts = await IOUtils.readJSON(TbSync.io.getAbsolutePath("accounts.json")); + for (let d of Object.values(accounts.data)) { + console.log("Migrating: " + JSON.stringify(d)); + + let settings = {}; + settings.status = "disabled"; + settings.provider = d.provider; + settings.https = (d.https == "1"); + + switch (d.provider) { + case "dav": + settings.calDavHost = d.host ? d.host : ""; + settings.cardDavHost = d.host2 ? d.host2 : ""; + settings.serviceprovider = d.serviceprovider; + settings.user = d.user; + settings.syncGroups = (d.syncGroups == "1"); + settings.useCalendarCache = (d.useCache == "1"); + break; + + case "eas": + settings.useragent = d.useragent; + settings.devicetype = d.devicetype; + settings.deviceId = getNewDeviceId4Migration(); + settings.asversionselected = d.asversionselected; + settings.asversion = d.asversion; + settings.host = d.host; + settings.user = d.user; + settings.servertype = d.servertype; + settings.seperator = d.seperator; + settings.provision = (d.provision == "1"); + settings.displayoverride = (d.displayoverride == "1"); + if (d.hasOwnProperty("galautocomplete")) settings.galautocomplete = (d.galautocomplete == "1"); + break; + } + + this.addAccount(d.accountname, settings); + } + } catch (e) { + Components.utils.reportError(e); + } + } + + this.loaded = true; + }, + + unload: async function () { + if (this.loaded) { + for (let f in this.files) { + try{ + //abort write delay timers and write current file content to disk + await this.files[f].write.finalize(); + } catch (e) { + Components.utils.reportError(e); + } + } + } + }, + + + saveFile: function (f) { + if (this.loaded) { + //cancel any pending write and schedule a new delayed write + this.files[f].write.disarm(); + this.files[f].write.arm(); + } + }, + + writeAsync: async function (f) { + // if this file was not found/read on load, do not write default content to prevent clearing of data in case of read-errors + if (!this.files[f].found && JSON.stringify(this[f]) == this.files[f].default) { + return; + } + + let filepath = TbSync.io.getAbsolutePath(this.files[f].name); + await IOUtils.writeJSON(filepath, this[f]); + }, + + + + // simple convenience wrapper + saveAccounts: function () { + this.saveFile("accounts"); + }, + + saveFolders: function () { + this.saveFile("folders"); + }, + + saveChangelog: function () { + this.saveFile("changelog"); + }, + + + + // CHANGELOG FUNCTIONS + getItemStatusFromChangeLog: function (parentId, itemId) { + for (let i=0; i<this.changelog.length; i++) { + if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) return this.changelog[i].status; + } + return null; + }, + + getItemDataFromChangeLog: function (parentId, itemId) { + for (let i=0; i<this.changelog.length; i++) { + if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) return this.changelog[i]; + } + return null; + }, + + addItemToChangeLog: function (parentId, itemId, status) { + this.removeItemFromChangeLog(parentId, itemId); + + //ChangelogData object + let row = { + "parentId" : parentId, + "itemId" : itemId, + "timestamp": Date.now(), + "status" : status}; + + this.changelog.push(row); + this.saveChangelog(); + }, + + removeItemFromChangeLog: function (parentId, itemId, moveToEnd = false) { + for (let i=this.changelog.length-1; i>-1; i-- ) { + if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) { + let row = this.changelog.splice(i,1); + if (moveToEnd) this.changelog.push(row[0]); + this.saveChangelog(); + return; + } + } + }, + + removeAllItemsFromChangeLogWithStatus: function (parentId, status) { + for (let i=this.changelog.length-1; i>-1; i-- ) { + if (this.changelog[i].parentId == parentId && this.changelog[i].status == status) { + let row = this.changelog.splice(i,1); + } + } + this.saveChangelog(); + }, + + // Remove all cards of a parentId from ChangeLog + clearChangeLog: function (parentId) { + if (parentId) { + // we allow extra parameters added to a parentId, but still want to delete all items of that parent + // so we check for startsWith instead of equal + for (let i=this.changelog.length-1; i>-1; i-- ) { + if (this.changelog[i].parentId.startsWith(parentId)) this.changelog.splice(i,1); + } + this.saveChangelog(); + } + }, + + getItemsFromChangeLog: function (parentId, maxnumbertosend, status = null) { + //maxnumbertosend = 0 will return all results + let log = []; + let counts = 0; + for (let i=0; i<this.changelog.length && (log.length < maxnumbertosend || maxnumbertosend == 0); i++) { + if (this.changelog[i].parentId == parentId && (status === null || (typeof this.changelog[i].status == "string" && this.changelog[i].status.indexOf(status) != -1))) log.push(this.changelog[i]); + } + return log; + }, + + + + + + // ACCOUNT FUNCTIONS + + addAccount: function (accountname, newAccountEntry) { + this.accounts.sequence++; + let id = this.accounts.sequence.toString(); + newAccountEntry.accountID = id; + newAccountEntry.accountname = accountname; + + this.accounts.data[id] = newAccountEntry; + this.saveAccounts(); + return id; + }, + + removeAccount: function (accountID) { + //check if accountID is known + if (this.accounts.data.hasOwnProperty(accountID) == false ) { + throw "Unknown accountID!" + "\nThrown by db.removeAccount("+accountID+ ")"; + } else { + delete (this.accounts.data[accountID]); + delete (this.folders[accountID]); + this.saveAccounts(); + this.saveFolders(); + } + }, + + getAccounts: function () { + let accounts = {}; + accounts.IDs = Object.keys(this.accounts.data).filter(accountID => TbSync.providers.loadedProviders.hasOwnProperty(this.accounts.data[accountID].provider)).sort((a, b) => a - b); + accounts.allIDs = Object.keys(this.accounts.data).sort((a, b) => a - b) + accounts.data = this.accounts.data; + return accounts; + }, + + getAccount: function (accountID) { + //check if accountID is known + if (this.accounts.data.hasOwnProperty(accountID) == false ) { + throw "Unknown accountID!" + "\nThrown by db.getAccount("+accountID+ ")"; + } else { + return this.accounts.data[accountID]; + } + }, + + isValidAccountProperty: function (provider, name) { + if (["provider"].includes(name)) //internal properties, do not need to be defined by user/provider + return true; + + //check if provider is installed + if (!TbSync.providers.loadedProviders.hasOwnProperty(provider)) { + TbSync.dump("Error @ isValidAccountProperty", "Unknown provider <"+provider+">!"); + return false; + } + + if (TbSync.providers.getDefaultAccountEntries(provider).hasOwnProperty(name)) { + return true; + } else { + TbSync.dump("Error @ isValidAccountProperty", "Unknown account setting <"+name+">!"); + return false; + } + }, + + getAccountProperty: function (accountID, name) { + // if the requested accountID does not exist, getAccount() will fail + let data = this.getAccount(accountID); + + //check if field is allowed and get value or default value if setting is not set + if (this.isValidAccountProperty(data.provider, name)) { + if (data.hasOwnProperty(name)) return data[name]; + else return TbSync.providers.getDefaultAccountEntries(data.provider)[name]; + } + }, + + setAccountProperty: function (accountID , name, value) { + // if the requested accountID does not exist, getAccount() will fail + let data = this.getAccount(accountID); + + //check if field is allowed, and set given value + if (this.isValidAccountProperty(data.provider, name)) { + this.accounts.data[accountID][name] = value; + } + this.saveAccounts(); + }, + + resetAccountProperty: function (accountID , name) { + // if the requested accountID does not exist, getAccount() will fail + let data = this.getAccount(accountID); + let defaults = TbSync.providers.getDefaultAccountEntries(data.provider); + + //check if field is allowed, and set given value + if (this.isValidAccountProperty(data.provider, name)) { + this.accounts.data[accountID][name] = defaults[name]; + } + this.saveAccounts(); + }, + + + + + // FOLDER FUNCTIONS + + addFolder: function(accountID) { + let folderID = TbSync.generateUUID(); + let provider = this.getAccountProperty(accountID, "provider"); + + if (!this.folders.hasOwnProperty(accountID)) this.folders[accountID] = {}; + + //create folder with default settings + this.folders[accountID][folderID] = TbSync.providers.getDefaultFolderEntries(accountID); + this.saveFolders(); + return folderID; + }, + + deleteFolder: function(accountID, folderID) { + delete (this.folders[accountID][folderID]); + //if there are no more folders, delete entire account entry + if (Object.keys(this.folders[accountID]).length === 0) delete (this.folders[accountID]); + this.saveFolders(); + }, + + isValidFolderProperty: function (accountID, field) { + if (["cached"].includes(field)) //internal properties, do not need to be defined by user/provider + return true; + + //check if provider is installed + let provider = this.getAccountProperty(accountID, "provider"); + if (!TbSync.providers.loadedProviders.hasOwnProperty(provider)) { + TbSync.dump("Error @ isValidFolderProperty", "Unknown provider <"+provider+"> for accountID <"+accountID+">!"); + return false; + } + + if (TbSync.providers.getDefaultFolderEntries(accountID).hasOwnProperty(field)) { + return true; + } else { + TbSync.dump("Error @ isValidFolderProperty", "Unknown folder setting <"+field+"> for accountID <"+accountID+">!"); + return false; + } + }, + + getFolderProperty: function(accountID, folderID, field) { + //does the field exist? + let folder = (this.folders.hasOwnProperty(accountID) && this.folders[accountID].hasOwnProperty(folderID)) ? this.folders[accountID][folderID] : null; + + if (folder === null) { + throw "Unknown folder <"+folderID+">!"; + } + + if (this.isValidFolderProperty(accountID, field)) { + if (folder.hasOwnProperty(field)) { + return folder[field]; + } else { + let provider = this.getAccountProperty(accountID, "provider"); + let defaultFolder = TbSync.providers.getDefaultFolderEntries(accountID); + //handle internal fields, that do not have a default value (see isValidFolderProperty) + return (defaultFolder[field] ? defaultFolder[field] : ""); + } + } + }, + + setFolderProperty: function (accountID, folderID, field, value) { + if (this.isValidFolderProperty(accountID, field)) { + this.folders[accountID][folderID][field] = value; + this.saveFolders(); + } + }, + + resetFolderProperty: function (accountID, folderID, field) { + let provider = this.getAccountProperty(accountID, "provider"); + let defaults = TbSync.providers.getDefaultFolderEntries(accountID); + if (this.isValidFolderProperty(accountID, field)) { + //handle internal fields, that do not have a default value (see isValidFolderProperty) + this.folders[accountID][folderID][field] = defaults[field] ? defaults[field] : ""; + this.saveFolders(); + } + }, + + findFolders: function (folderQuery = {}, accountQuery = {}) { + // folderQuery is an object with one or more key:value pairs (logical AND) :: + // {key1: value1, key2: value2} + // the value itself may be an array (logical OR) + let data = []; + let folderQueryEntries = Object.entries(folderQuery); + let folderFields = folderQueryEntries.map(pair => pair[0]); + let folderValues = folderQueryEntries.map(pair => Array.isArray(pair[1]) ? pair[1] : [pair[1]]); + + let accountQueryEntries = Object.entries(accountQuery); + let accountFields = accountQueryEntries.map(pair => pair[0]); + let accountValues = accountQueryEntries.map(pair => Array.isArray(pair[1]) ? pair[1] : [pair[1]]); + + for (let aID in this.folders) { + //is this a leftover folder of an account, which no longer there? + if (!this.accounts.data.hasOwnProperty(aID)) { + delete (this.folders[aID]); + this.saveFolders(); + continue; + } + + //skip this folder, if it belongs to an account currently not supported (provider not loaded) + if (!TbSync.providers.loadedProviders.hasOwnProperty(this.getAccountProperty(aID, "provider"))) { + continue; + } + + //does this account match account search options? + let accountmatch = true; + for (let a = 0; a < accountFields.length && accountmatch; a++) { + accountmatch = accountValues[a].some(item => item === this.getAccountProperty(aID, accountFields[a])); + //Services.console.logStringMessage(" " + accountFields[a] + ":" + this.getAccountProperty(aID, accountFields[a]) + " in " + JSON.stringify(accountValues[a]) + " ? " + accountmatch); + } + + if (accountmatch) { + for (let fID in this.folders[aID]) { + //does this folder match folder search options? + let foldermatch = true; + for (let f = 0; f < folderFields.length && foldermatch; f++) { + foldermatch = folderValues[f].some(item => item === this.getFolderProperty(aID, fID, folderFields[f])); + //Services.console.logStringMessage(" " + folderFields[f] + ":" + this.getFolderProperty(aID, fID, folderFields[f]) + " in " + JSON.stringify(folderValues[f]) + " ? " + foldermatch); + } + if (foldermatch) data.push({accountID: aID, folderID: fID, data: this.folders[aID][fID]}); + } + } + } + + //still a reference to the original data + return data; + } +}; diff --git a/content/modules/eventlog.js b/content/modules/eventlog.js new file mode 100644 index 0000000..ef8f1e8 --- /dev/null +++ b/content/modules/eventlog.js @@ -0,0 +1,153 @@ +/* + * This file is part of TbSync. + * + * 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 EventLogInfo = class { + /** + * An EventLogInfo instance is used when adding entries to the + * :ref:`TbSyncEventLog`. The information given here will be added as a + * header to the actual event. + * + * @param {string} provider ``Optional`` A provider ID (also used as + * provider namespace). + * @param {string} accountname ``Optional`` An account name. Can be + * arbitrary but should match the accountID + * (if provided). + * @param {string} accountID ``Optional`` An account ID. Used to filter + * events for a given account. + * @param {string} foldername ``Optional`` A folder name. + * + */ + constructor(provider, accountname = "", accountID = "", foldername = "") { + this._provider = provider; + this._accountname = accountname; + this._accountID = accountID; + this._foldername = foldername; + } + + /** + * Getter/Setter for the provider ID of this EventLogInfo. + */ + get provider() {return this._provider}; + /** + * Getter/Setter for the account ID of this EventLogInfo. + */ + get accountname() {return this._accountname}; + /** + * Getter/Setter for the account name of this EventLogInfo. + */ + get accountID() {return this._accountID}; + /** + * Getter/Setter for the folder name of this EventLogInfo. + */ + get foldername() {return this._foldername}; + + set provider(v) {this._provider = v}; + set accountname(v) {this._accountname = v}; + set accountID(v) {this._accountID = v}; + set foldername(v) {this._foldername = v}; +} + + + +/** + * The TbSync event log + */ +var eventlog = { + /** + * Adds an entry to the TbSync event log + * + * @param {StatusDataType} type One of the types defined in + * :class:`StatusData` + * @param {EventLogInfo} eventInfo EventLogInfo for this event. + * @param {string} message The event message. + * @param {string} details ``Optional`` The event details. + * + */ + add: function (type, eventInfo, message, details = null) { + let entry = { + timestamp: Date.now(), + message: message, + type: type, + link: null, + //some details are just true, which is not a useful detail, ignore + details: details === true ? null : details, + provider: "", + accountname: "", + foldername: "", + }; + + if (eventInfo) { + if (eventInfo.accountID) entry.accountID = eventInfo.accountID; + if (eventInfo.provider) entry.provider = eventInfo.provider; + if (eventInfo.accountname) entry.accountname = eventInfo.accountname; + if (eventInfo.foldername) entry.foldername = eventInfo.foldername; + } + + let localized = ""; + let link = ""; + if (entry.provider) { + localized = TbSync.getString("status." + message, entry.provider); + link = TbSync.getString("helplink." + message, entry.provider); + } else { + //try to get localized string from message from TbSync + localized = TbSync.getString("status." + message); + link = TbSync.getString("helplink." + message); + } + + //can we provide a localized version of the event msg? + if (localized != "status."+message) { + entry.message = localized; + } + + //is there a help link? + if (link != "helplink." + message) { + entry.link = link; + } + + //dump the non-localized message into debug log + TbSync.dump("EventLog", message + (entry.details !== null ? "\n" + entry.details : "")); + this.events.push(entry); + if (this.events.length > 100) this.events.shift(); + Services.obs.notifyObservers(null, "tbsync.observer.eventlog.update", null); + }, + + events: null, + eventLogWindow: null, + + load: async function () { + this.clear(); + }, + + unload: async function () { + if (this.eventLogWindow) { + this.eventLogWindow.close(); + } + }, + + get: function (accountID = null) { + if (accountID) { + return this.events.filter(e => e.accountID == accountID); + } else { + return this.events; + } + }, + + clear: function () { + this.events = []; + }, + + + open: function (accountID = null, folderID = null) { + this.eventLogWindow = TbSync.manager.prefWindowObj.open("chrome://tbsync/content/manager/eventlog/eventlog.xhtml", "TbSyncEventLog", "centerscreen,chrome,resizable"); + }, +} diff --git a/content/modules/io.js b/content/modules/io.js new file mode 100644 index 0000000..a4ecbdb --- /dev/null +++ b/content/modules/io.js @@ -0,0 +1,41 @@ +/* + * This file is part of TbSync. + * + * 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 io = { + + storageDirectory : PathUtils.join(PathUtils.profileDir, "TbSync"), + + load: async function () { + }, + + unload: async function () { + }, + + getAbsolutePath: function(filename) { + return PathUtils.join(this.storageDirectory, filename); + }, + + initFile: function (filename) { + let file = FileUtils.getFile("ProfD", ["TbSync",filename]); + //create a stream to write to that file + let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream); + foStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0666", 8), 0); // write, create, truncate + foStream.close(); + }, + + appendToFile: function (filename, data) { + let file = FileUtils.getFile("ProfD", ["TbSync",filename]); + //create a strem to write to that file + let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream); + foStream.init(file, 0x02 | 0x08 | 0x10, parseInt("0666", 8), 0); // write, create, append + foStream.write(data, data.length); + foStream.close(); + }, +} diff --git a/content/modules/lightning.js b/content/modules/lightning.js new file mode 100644 index 0000000..cd9a383 --- /dev/null +++ b/content/modules/lightning.js @@ -0,0 +1,774 @@ +/* + * This file is part of TbSync. + * + * 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 { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + + XPCOMUtils.defineLazyModuleGetters(this, { + CalAlarm: "resource:///modules/CalAlarm.jsm", + CalAttachment: "resource:///modules/CalAttachment.jsm", + CalAttendee: "resource:///modules/CalAttendee.jsm", + CalEvent: "resource:///modules/CalEvent.jsm", + CalTodo: "resource:///modules/CalTodo.jsm", +}); + +var lightning = { + + cal: null, + ICAL: null, + + load: async function () { + try { + TbSync.lightning.cal = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm").cal; + TbSync.lightning.ICAL = ChromeUtils.import("resource:///modules/calendar/Ical.jsm").ICAL; + let manager = TbSync.lightning.cal.manager; + manager.addCalendarObserver(this.calendarObserver); + manager.addObserver(this.calendarManagerObserver); + } catch (e) { + TbSync.dump("Check4Lightning","Error during lightning module import: " + e.toString() + "\n" + e.stack); + Components.utils.reportError(e); + } + }, + + unload: async function () { + //removing global observer + let manager = TbSync.lightning.cal.manager; + manager.removeCalendarObserver(this.calendarObserver); + manager.removeObserver(this.calendarManagerObserver); + + //remove listeners on global sync buttons + if (TbSync.window.document.getElementById("calendar-synchronize-button")) { + TbSync.window.document.getElementById("calendar-synchronize-button").removeEventListener("click", function(event){Services.obs.notifyObservers(null, 'tbsync.observer.sync', null);}, false); + } + if (TbSync.window.document.getElementById("task-synchronize-button")) { + TbSync.window.document.getElementById("task-synchronize-button").removeEventListener("click", function(event){Services.obs.notifyObservers(null, 'tbsync.observer.sync', null);}, false); + } + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * AdvancedTargetData, an extended TargetData implementation, providers + // * can use this as their own TargetData by extending it and just + // * defining the extra methods + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + AdvancedTargetData : class { + constructor(folderData) { + this._folderData = folderData; + this._targetObj = null; + } + + // Check, if the target exists and return true/false. + hasTarget() { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + return calendar ? true : false; + } + + // Returns the target obj, which TbSync should return as the target. It can + // be whatever you want and is returned by FolderData.targetData.getTarget(). + // If the target does not exist, it should be created. Throw a simple Error, if that + // failed. + async getTarget() { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + if (!calendar) { + calendar = await TbSync.lightning.prepareAndCreateCalendar(this._folderData); + if (!calendar) + throw new Error("notargets"); + } + + if (!this._targetObj || this._targetObj.id != calendar.id) + this._targetObj = new TbSync.lightning.TbCalendar(calendar, this._folderData); + + return this._targetObj; + } + + /** + * Removes the target from the local storage. If it does not exist, return + * silently. A call to ``hasTarget()`` should return false, after this has + * been executed. + * + */ + removeTarget() { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + try { + if (calendar) { + calManager.removeCalendar(calendar); + } + } catch (e) {} + TbSync.db.clearChangeLog(target); + this._folderData.resetFolderProperty("target"); + } + + + /** + * Disconnects the target in the local storage from this TargetData, but + * does not delete it, so it becomes a stale "left over" . A call + * to ``hasTarget()`` should return false, after this has been executed. + * + */ + disconnectTarget() { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + if (calendar) { + let changes = TbSync.db.getItemsFromChangeLog(target, 0, "_by_user"); + if (changes.length > 0) { + this.targetName = this.targetName + " (*)"; + } + calendar.setProperty("disabled", true); + calendar.setProperty("tbSyncProvider", "orphaned"); + calendar.setProperty("tbSyncAccountID", ""); + } + TbSync.db.clearChangeLog(target); + this._folderData.resetFolderProperty("target"); + } + + set targetName(newName) { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + if (calendar) { + calendar.name = newName; + } else { + throw new Error("notargets"); + } + } + + get targetName() { + let calManager = TbSync.lightning.cal.manager; + let target = this._folderData.getFolderProperty("target"); + let calendar = calManager.getCalendarById(target); + + if (calendar) { + return calendar.name; + } else { + throw new Error("notargets"); + } + } + + setReadOnly(value) { + // hasTarget() can throw an error, ignore that here + try { + if (this.hasTarget()) { + this.getTarget().then(target => target.calendar.setProperty("readOnly", value)); + } + } catch (e) { + Components.utils.reportError(e); + } + } + + + // * * * * * * * * * * * * * * * * * + // * AdvancedTargetData extension * + // * * * * * * * * * * * * * * * * * + + get isAdvancedCalendarTargetData() { + return true; + } + + get folderData() { + return this._folderData; + } + + // The calendar target does not support a custom primaryKeyField, because + // the lightning implementation only allows to search for items via UID. + // Like the addressbook target, the calendar target item element has a + // primaryKey getter/setter which - however - only works on the UID. + + // enable or disable changelog + get logUserChanges(){ + return true; + } + + calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) { + switch (aTopic) { + case "onCalendarPropertyChanged": + //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName); + break; + + case "onCalendarDeleted": + case "onCalendarPropertyDeleted": + //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name); + break; + } + } + + itemObserver(aTopic, tbItem, tbOldItem) { + switch (aTopic) { + case "onAddItem": + case "onModifyItem": + case "onDeleteItem": + //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title); + break; + } + } + + // replace this with your own implementation to create the actual addressbook, + // when this class is extended + async createCalendar(newname) { + let calManager = TbSync.lightning.cal.manager; + let newCalendar = calManager.createCalendar("storage", Services.io.newURI("moz-storage-calendar://")); + newCalendar.id = TbSync.lightning.cal.getUUID(); + newCalendar.name = newname; + return newCalendar + } + + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * TbItem and TbCalendar Classes + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + TbItem : class { + constructor(TbCalendar, item) { + if (!TbCalendar) + throw new Error("TbItem::constructor is missing its first parameter!"); + + if (!item) + throw new Error("TbItem::constructor is missing its second parameter!"); + + this._tbCalendar = TbCalendar; + this._item = item; + + this._isTodo = (item instanceof Ci.calITodo); + this._isEvent = (item instanceof Ci.calIEvent); + } + + get tbCalendar() { + return this._tbCalendar; + } + + get isTodo() { + return this._isTodo; + } + + get isEvent() { + return this._isEvent; + } + + + + + + get nativeItem() { + return this._item; + } + + get UID() { + return this._item.id; + } + + get primaryKey() { + // no custom key possible with lightning, must use the UID + return this._item.id; + } + + set primaryKey(value) { + // no custom key possible with lightning, must use the UID + this._item.id = value; + } + + clone() { + return new TbSync.lightning.TbItem(this._tbCalendar, this._item.clone()); + } + + toString() { + return this._item.icalString; + } + + getProperty(property, fallback = "") { + return this._item.hasProperty(property) ? this._item.getProperty(property) : fallback; + } + + setProperty(property, value) { + this._item.setProperty(property, value); + } + + deleteProperty(property) { + this._item.deleteProperty(property); + } + + get changelogData() { + return TbSync.db.getItemDataFromChangeLog(this._tbCalendar.UID, this.primaryKey); + } + + get changelogStatus() { + return TbSync.db.getItemStatusFromChangeLog(this._tbCalendar.UID, this.primaryKey); + } + + set changelogStatus(status) { + let value = this.primaryKey; + + if (value) { + if (!status) { + TbSync.db.removeItemFromChangeLog(this._tbCalendar.UID, value); + return; + } + + if (this._tbCalendar.logUserChanges || status.endsWith("_by_server")) { + TbSync.db.addItemToChangeLog(this._tbCalendar.UID, value, status); + } + } + } + }, + + + TbCalendar : class { + constructor(calendar, folderData) { + this._calendar = calendar; + this._folderData = folderData; + } + + get calendar() { + return this._calendar; + } + + get promisifyCalendar() { + return this._calendar; + } + + get logUserChanges() { + return this._folderData.targetData.logUserChanges; + } + + get primaryKeyField() { + // Not supported by lightning. We let the implementation sit here, it may get changed in the future. + // In order to support this, lightning needs to implement a proper getItemfromProperty() method. + return null; + } + + get UID() { + return this._calendar.id; + } + + createNewEvent() { + let event = new CalEvent(); + return new TbSync.lightning.TbItem(this, event); + } + + createNewTodo() { + let todo = new CalTodo(); + return new TbSync.lightning.TbItem(this, todo); + } + + + + + async addItem(tbItem, pretagChangelogWithByServerEntry = true) { + if (this.primaryKeyField && !tbItem.getProperty(this.primaryKeyField)) { + tbItem.setProperty(this.primaryKeyField, this._folderData.targetData.generatePrimaryKey()); + //Services.console.logStringMessage("[TbCalendar::addItem] Generated primary key!"); + } + + if (pretagChangelogWithByServerEntry) { + tbItem.changelogStatus = "added_by_server"; + } + return await this._calendar.addItem(tbItem._item); + } + + async modifyItem(tbNewItem, tbOldItem, pretagChangelogWithByServerEntry = true) { + // only add entry if the current entry does not start with _by_user + let status = tbNewItem.changelogStatus ? tbNewItem.changelogStatus : ""; + if (pretagChangelogWithByServerEntry && !status.endsWith("_by_user")) { + tbNewItem.changelogStatus = "modified_by_server"; + } + + return await this._calendar.modifyItem(tbNewItem._item, tbOldItem._item); + } + + async deleteItem(tbItem, pretagChangelogWithByServerEntry = true) { + if (pretagChangelogWithByServerEntry) { + tbItem.changelogStatus = "deleted_by_server"; + } + return await this._calendar.deleteItem(tbItem._item); + } + + // searchId is interpreted as the primaryKeyField, which is the UID for this target + async getItem (searchId) { + let item = await this._calendar.getItem(searchId); + if (item) { + return new TbSync.lightning.TbItem(this, item); + } + return null; + } + + async getItemFromProperty(property, value) { + if (property == "UID") return await this.getItem(value); + else throw ("TbSync.lightning.getItemFromProperty: Currently onle the UID property can be used to search for items."); + } + + async getAllItems() { + return await this._calendar.getItems(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null); + } + + getAddedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "added_by_user").map(item => item.itemId); + } + + getModifiedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "modified_by_user").map(item => item.itemId); + } + + getDeletedItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "deleted_by_user").map(item => item.itemId); + } + + getItemsFromChangeLog(maxitems = 0) { + return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "_by_user"); + } + + removeItemFromChangeLog(id, moveToEndInsteadOfDelete = false) { + TbSync.db.removeItemFromChangeLog(this.calendar.id, id, moveToEndInsteadOfDelete); + } + + clearChangelog() { + TbSync.db.clearChangeLog(this.calendar.id); + } + }, + + + + + + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + // * Internal Functions + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + + getFolderFromCalendarUID: function(calUID) { + let folders = TbSync.db.findFolders({"target": calUID}); + if (folders.length == 1) { + let accountData = new TbSync.AccountData(folders[0].accountID); + return new TbSync.FolderData(accountData, folders[0].folderID); + } + return null; + }, + + getFolderFromCalendarURL: function(calURL) { + let folders = TbSync.db.findFolders({"url": calURL}); + if (folders.length == 1) { + let accountData = new TbSync.AccountData(folders[0].accountID); + return new TbSync.FolderData(accountData, folders[0].folderID); + } + return null; + }, + + calendarObserver : { + onStartBatch : function () {}, + onEndBatch : function () {}, + onLoad : function (aCalendar) {}, + onError : function (aCalendar, aErrNo, aMessage) {}, + + onAddItem : function (aAddedItem) { + if (!(aAddedItem && aAddedItem.calendar)) + return; + + let folderData = TbSync.lightning.getFolderFromCalendarUID(aAddedItem.calendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + let tbCalendar = new TbSync.lightning.TbCalendar(aAddedItem.calendar, folderData); + let tbItem = new TbSync.lightning.TbItem(tbCalendar, aAddedItem); + let itemStatus = tbItem.changelogStatus; + + // if this card was created by us, it will be in the log + if (itemStatus && itemStatus.endsWith("_by_server")) { + let age = Date.now() - tbItem.changelogData.timestamp; + if (age < 1500) { + // during freeze, local modifications are not possible + return; + } else { + // remove blocking entry from changelog after freeze time is over (1.5s), + // and continue evaluating this event + abItem.changelogStatus = ""; + } + } + + if (itemStatus == "deleted_by_user") { + // deleted ? user moved item out and back in -> modified + tbItem.changelogStatus = "modified_by_user"; + } else { + tbItem.changelogStatus = "added_by_user"; + } + + if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.itemObserver("onAddItem", tbItem, null); + } + }, + + onModifyItem : function (aNewItem, aOldItem) { + //check, if it is a pure modification within the same calendar + if (!(aNewItem && aNewItem.calendar && aOldItem && aOldItem.calendar && aNewItem.calendar.id == aOldItem.calendar.id)) + return; + + let folderData = TbSync.lightning.getFolderFromCalendarUID(aNewItem.calendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + let tbCalendar = new TbSync.lightning.TbCalendar(aNewItem.calendar, folderData); + let tbNewItem = new TbSync.lightning.TbItem(tbCalendar, aNewItem); + let tbOldItem = new TbSync.lightning.TbItem(tbCalendar, aOldItem); + let itemStatus = tbNewItem.changelogStatus; + + // if this card was created by us, it will be in the log + if (itemStatus && itemStatus.endsWith("_by_server")) { + let age = Date.now() - tbNewItem.changelogData.timestamp; + if (age < 1500) { + // during freeze, local modifications are not possible + return; + } else { + // remove blocking entry from changelog after freeze time is over (1.5s), + // and continue evaluating this event + tbNewItem.changelogStatus = ""; + } + } + + if (itemStatus != "added_by_user") { + //added_by_user -> it is a local unprocessed add do not re-add it to changelog + tbNewItem.changelogStatus = "modified_by_user"; + } + + if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.itemObserver("onModifyItem", tbNewItem, tbOldItem); + } + }, + + onDeleteItem : function (aDeletedItem) { + if (!(aDeletedItem && aDeletedItem.calendar)) + return; + + let folderData = TbSync.lightning.getFolderFromCalendarUID(aDeletedItem.calendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + let tbCalendar = new TbSync.lightning.TbCalendar(aDeletedItem.calendar, folderData); + let tbItem = new TbSync.lightning.TbItem(tbCalendar, aDeletedItem); + let itemStatus = tbItem.changelogStatus; + + // if this card was created by us, it will be in the log + if (itemStatus && itemStatus.endsWith("_by_server")) { + let age = Date.now() - tbItem.changelogData.timestamp; + if (age < 1500) { + // during freeze, local modifications are not possible + return; + } else { + // remove blocking entry from changelog after freeze time is over (1.5s), + // and continue evaluating this event + tbItem.changelogStatus = ""; + } + } + + if (itemStatus == "added_by_user") { + //a local add, which has not yet been processed (synced) is deleted -> remove all traces + tbItem.changelogStatus = ""; + } else { + tbItem.changelogStatus = "deleted_by_user"; + } + + if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData); + folderData.targetData.itemObserver("onDeleteItem", tbItem, null); + } + }, + + //Changed properties of the calendar itself (name, color etc.) + onPropertyChanged : function (aCalendar, aName, aValue, aOldValue) { + let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData); + + switch (aName) { + case "color": + // update stored color to recover after disable + folderData.setFolderProperty("targetColor", aValue); + break; + case "name": + // update stored name to recover after disable + folderData.setFolderProperty("targetName", aValue); + // update settings window, if open + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + break; + } + + folderData.targetData.calendarObserver("onCalendarPropertyChanged", tbCalendar, aName, aValue, aOldValue); + } + }, + + //Deleted properties of the calendar itself (name, color etc.) + onPropertyDeleting : function (aCalendar, aName) { + let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData); + + switch (aName) { + case "color": + case "name": + //update settings window, if open + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + break; + } + + folderData.targetData.calendarObserver("onCalendarPropertyDeleted", tbCalendar, aName); + } + } + }, + + calendarManagerObserver : { + onCalendarRegistered : function (aCalendar) { + }, + + onCalendarUnregistering : function (aCalendar) { + /*let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + folderData.targetData.calendarObserver("onCalendarUnregistered", aCalendar); + }*/ + }, + + onCalendarDeleting : async function (aCalendar) { + let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id); + if (folderData + && folderData.targetData + && folderData.targetData.isAdvancedCalendarTargetData) { + + // If the user switches "offline support", the calendar is deleted and recreated. Thus, + // we wait a bit and check, if the calendar is back again and ignore the delete event. + if (aCalendar.type == "caldav") { + await TbSync.tools.sleep(1500); + let calManager = TbSync.lightning.cal.manager; + for (let calendar of calManager.getCalendars({})) { + if (calendar.uri.spec == aCalendar.uri.spec) { + // update the target + folderData.setFolderProperty("target", calendar.id) + return; + } + } + } + + //delete any pending changelog of the deleted calendar + TbSync.db.clearChangeLog(aCalendar.id); + + let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData); + + //unselect calendar if deleted by user and update settings window, if open + if (folderData.getFolderProperty("selected")) { + folderData.setFolderProperty("selected", false); + //update settings window, if open + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID); + } + + folderData.resetFolderProperty("target"); + folderData.targetData.calendarObserver("onCalendarDeleted", tbCalendar); + + } + }, + }, + + + + //this function actually creates a calendar if missing + prepareAndCreateCalendar: async function (folderData) { + let calManager = TbSync.lightning.cal.manager; + let provider = folderData.accountData.getAccountProperty("provider"); + + //check if there is a known/cached name, and use that as starting point to generate unique name for new calendar + let cachedName = folderData.getFolderProperty("targetName"); + let newname = cachedName == "" ? folderData.accountData.getAccountProperty("accountname") + " (" + folderData.getFolderProperty("foldername") + ")" : cachedName; + + //check if there is a cached or preloaded color - if not, chose one + if (!folderData.getFolderProperty("targetColor")) { + //define color set + let allColors = [ + "#3366CC", + "#DC3912", + "#FF9900", + "#109618", + "#990099", + "#3B3EAC", + "#0099C6", + "#DD4477", + "#66AA00", + "#B82E2E", + "#316395", + "#994499", + "#22AA99", + "#AAAA11", + "#6633CC", + "#E67300", + "#8B0707", + "#329262", + "#5574A6", + "#3B3EAC"]; + + //find all used colors + let usedColors = []; + for (let calendar of calManager.getCalendars({})) { + if (calendar && calendar.getProperty("color")) { + usedColors.push(calendar.getProperty("color").toUpperCase()); + } + } + + //we do not want to change order of colors, we want to FILTER by counts, so we need to find the least count, filter by that and then take the first one + let minCount = null; + let statColors = []; + for (let i=0; i< allColors.length; i++) { + let count = usedColors.filter(item => item == allColors[i]).length; + if (minCount === null) minCount = count; + else if (count < minCount) minCount = count; + + let obj = {}; + obj.color = allColors[i]; + obj.count = count; + statColors.push(obj); + } + + //filter by minCount + let freeColors = statColors.filter(item => (minCount == null || item.count == minCount)); + folderData.setFolderProperty("targetColor", freeColors[0].color); + } + + //create and register new calendar + let newCalendar = await folderData.targetData.createCalendar(newname); + newCalendar.setProperty("tbSyncProvider", provider); + newCalendar.setProperty("tbSyncAccountID", folderData.accountData.accountID); + + //store id of calendar as target in DB + folderData.setFolderProperty("target", newCalendar.id); + folderData.setFolderProperty("targetName", newCalendar.name); + folderData.setFolderProperty("targetColor", newCalendar.getProperty("color")); + return newCalendar; + } +} diff --git a/content/modules/manager.js b/content/modules/manager.js new file mode 100644 index 0000000..12ef4b2 --- /dev/null +++ b/content/modules/manager.js @@ -0,0 +1,392 @@ +/* + * This file is part of TbSync. + * + * 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 manager = { + + prefWindowObj: null, + + load: async function () { + }, + + unload: async function () { + //close window (if open) + if (this.prefWindowObj !== null) this.prefWindowObj.close(); + }, + + + + + + openManagerWindow: function(event) { + if (!event.button) { //catches zero or undefined + if (TbSync.enabled) { + // check, if a window is already open and just put it in focus + if (this.prefWindowObj === null) { + this.prefWindowObj = TbSync.window.open("chrome://tbsync/content/manager/accountManager.xhtml", "TbSyncAccountManagerWindow", "chrome,centerscreen"); + } + this.prefWindowObj.focus(); + } else { + //this.popupNotEnabled(); + } + } + }, + + popupNotEnabled: function () { + TbSync.dump("Oops", "Trying to open account manager, but init sequence not yet finished"); + let msg = TbSync.getString("OopsMessage") + "\n\n"; + let v = Services.appinfo.platformVersion; + if (TbSync.prefs.getIntPref("log.userdatalevel") == 0) { + if (TbSync.window.confirm(msg + TbSync.getString("UnableToTraceError"))) { + TbSync.prefs.setIntPref("log.userdatalevel", 1); + TbSync.window.alert(TbSync.getString("RestartThunderbirdAndTryAgain")); + } + } else { + if (TbSync.window.confirm(msg + TbSync.getString("HelpFixStartupError"))) { + this.createBugReport("john.bieling@gmx.de", msg, ""); + } + } + }, + + openTBtab: function (url) { + let tabmail = TbSync.window.document.getElementById("tabmail"); + if (TbSync.window && tabmail) { + TbSync.window.focus(); + return tabmail.openTab("contentTab", { + url + }); + } + return null; + }, + + openTranslatedLink: function (url) { + let googleCode = TbSync.getString("google.translate.code"); + if (googleCode != "en" && googleCode != "google.translate.code") { + this.openLink("https://translate.google.com/translate?hl=en&sl=en&tl="+TbSync.getString("google.translate.code")+"&u="+url); + } else { + this.openLink(url); + } + }, + + openLink: function (url) { + let ioservice = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); + let uriToOpen = ioservice.newURI(url, null, null); + let extps = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService); + extps.loadURI(uriToOpen, null); + }, + + openBugReportWizard: function () { + if (!TbSync.debugMode) { + this.prefWindowObj.alert(TbSync.getString("NoDebugLog")); + } else { + this.prefWindowObj.openDialog("chrome://tbsync/content/manager/support-wizard/support-wizard.xhtml", "support-wizard", "dialog,centerscreen,chrome,resizable=no"); + } + }, + + createBugReport: function (email, subject, description) { + let fields = Components.classes["@mozilla.org/messengercompose/composefields;1"].createInstance(Components.interfaces.nsIMsgCompFields); + let params = Components.classes["@mozilla.org/messengercompose/composeparams;1"].createInstance(Components.interfaces.nsIMsgComposeParams); + + fields.to = email; + fields.subject = "TbSync " + TbSync.addon.version.toString() + " bug report: " + subject; + fields.body = "Hi,\n\n" + + "attached you find my debug.log for the following error:\n\n" + + description; + + params.composeFields = fields; + params.format = Components.interfaces.nsIMsgCompFormat.PlainText; + + let attachment = Components.classes["@mozilla.org/messengercompose/attachment;1"].createInstance(Components.interfaces.nsIMsgAttachment); + attachment.contentType = "text/plain"; + attachment.url = 'file://' + TbSync.io.getAbsolutePath("debug.log"); + attachment.name = "debug.log"; + attachment.temporary = false; + + params.composeFields.addAttachment(attachment); + MailServices.compose.OpenComposeWindowWithParams (null, params); + }, + + viewDebugLog: function() { + + if (this.debugLogWindow && this.debugLogWindow.tabNode) { + let tabmail = TbSync.window.document.getElementById("tabmail"); + try { + tabmail.closeTab(this.debugLogWindow); + } catch (e) { + // nope + } + this.debugLogWindow = null; + } + this.debugLogWindow = this.openTBtab('file://' + TbSync.io.getAbsolutePath("debug.log")); + }, +} + + + +/** + * Functions used by the folderlist in the main account settings tab + */ +manager.FolderList = class { + /** + * @param {string} provider Identifier for the provider this FolderListView is created for. + */ + constructor(provider) { + this.provider = provider + } + + /** + * Is called before the context menu of the folderlist is shown, allows to + * show/hide custom menu options based on selected folder + * + * @param document [in] document object of the account settings window - element.ownerDocument - menuentry? + * @param folderData [in] FolderData of the selected folder + */ + onContextMenuShowing(window, folderData) { + return TbSync.providers[this.provider].StandardFolderList.onContextMenuShowing(window, folderData); + } + + + /** + * Returns an array of attribute objects, which define the number of columns + * and the look of the header + */ + getHeader() { + return [ + {style: "font-weight:bold;", label: "", width: "93"}, + {style: "font-weight:bold;", label: TbSync.getString("manager.resource"), width:"150"}, + {style: "font-weight:bold;", label: TbSync.getString("manager.status"), flex :"1"}, + ] + } + + + /** + * Is called to add a row to the folderlist. After this call, updateRow is called as well. + * + * @param document [in] document object of the account settings window + * @param folderData [in] FolderData of the folder in the row + */ + getRow(document, folderData) { + //create checkBox for select state + let itemSelCheckbox = document.createXULElement("checkbox"); + itemSelCheckbox.setAttribute("updatefield", "selectbox"); + itemSelCheckbox.setAttribute("style", "margin: 0px 0px 0px 3px;"); + itemSelCheckbox.addEventListener("command", this.toggleFolder); + + //icon + let itemType = document.createXULElement("image"); + itemType.setAttribute("src", TbSync.providers[this.provider].StandardFolderList.getTypeImage(folderData)); + itemType.setAttribute("style", "margin: 0px 9px 0px 3px;"); + + //ACL + let roAttributes = TbSync.providers[this.provider].StandardFolderList.getAttributesRoAcl(folderData); + let rwAttributes = TbSync.providers[this.provider].StandardFolderList.getAttributesRwAcl(folderData); + let itemACL = document.createXULElement("button"); + itemACL.setAttribute("image", "chrome://tbsync/content/skin/acl_" + (folderData.getFolderProperty("downloadonly") ? "ro" : "rw") + ".png"); + itemACL.setAttribute("class", "plain"); + itemACL.setAttribute("style", "width: 35px; min-width: 35px; margin: 0; height:26px"); + itemACL.setAttribute("updatefield", "acl"); + if (roAttributes && rwAttributes) { + itemACL.setAttribute("type", "menu"); + let menupopup = document.createXULElement("menupopup"); + { + let menuitem = document.createXULElement("menuitem"); + menuitem.downloadonly = false; + menuitem.setAttribute("class", "menuitem-iconic"); + menuitem.setAttribute("image", "chrome://tbsync/content/skin/acl_rw2.png"); + menuitem.addEventListener("command", this.updateReadOnly); + for (const [attr, value] of Object.entries(rwAttributes)) { + menuitem.setAttribute(attr, value); + } + menupopup.appendChild(menuitem); + } + + { + let menuitem = document.createXULElement("menuitem"); + menuitem.downloadonly = true; + menuitem.setAttribute("class", "menuitem-iconic"); + menuitem.setAttribute("image", "chrome://tbsync/content/skin/acl_ro2.png"); + menuitem.addEventListener("command", this.updateReadOnly); + for (const [attr, value] of Object.entries(roAttributes)) { + menuitem.setAttribute(attr, value); + } + menupopup.appendChild(menuitem); + } + itemACL.appendChild(menupopup); + } + + //folder name + let itemLabel = document.createXULElement("description"); + itemLabel.setAttribute("updatefield", "foldername"); + + //status + let itemStatus = document.createXULElement("description"); + itemStatus.setAttribute("updatefield", "status"); + + //group1 + let itemHGroup1 = document.createXULElement("hbox"); + itemHGroup1.setAttribute("align", "center"); + itemHGroup1.appendChild(itemSelCheckbox); + itemHGroup1.appendChild(itemType); + if (itemACL) itemHGroup1.appendChild(itemACL); + + let itemVGroup1 = document.createXULElement("vbox"); + //itemVGroup1.setAttribute("width", "93"); + itemVGroup1.setAttribute("style", "width: 93px"); + itemVGroup1.appendChild(itemHGroup1); + + //group2 + let itemHGroup2 = document.createXULElement("hbox"); + itemHGroup2.setAttribute("align", "center"); + itemHGroup2.setAttribute("style", "border: 1px center"); + itemHGroup2.appendChild(itemLabel); + + let itemVGroup2 = document.createXULElement("vbox"); + //itemVGroup2.setAttribute("width", "150"); + itemVGroup2.setAttribute("style", "padding: 3px; width: 150px"); + itemVGroup2.appendChild(itemHGroup2); + + //group3 + let itemHGroup3 = document.createXULElement("hbox"); + itemHGroup3.setAttribute("align", "center"); + itemHGroup3.appendChild(itemStatus); + + let itemVGroup3 = document.createXULElement("vbox"); + //itemVGroup3.setAttribute("width", "250"); + itemVGroup3.setAttribute("style", "padding: 3px; width: 250px"); + itemVGroup3.appendChild(itemHGroup3); + + //final row + let row = document.createXULElement("hbox"); + row.setAttribute("style", "min-height: 24px;"); + row.appendChild(itemVGroup1); + row.appendChild(itemVGroup2); + row.appendChild(itemVGroup3); + return row; + } + + + /** + * ToggleFolder event + */ + toggleFolder(event) { + let element = event.target; + let folderList = element.ownerDocument.getElementById("tbsync.accountsettings.folderlist"); + if (folderList.selectedItem !== null && !folderList.disabled) { + // the folderData obj of the selected folder is attached to its row entry + let folder = folderList.selectedItem.folderData; + + if (!folder.accountData.isEnabled()) + return; + + if (folder.getFolderProperty("selected")) { + // hasTarget() can throw an error, ignore that here + try { + if (!folder.targetData.hasTarget() || element.ownerDocument.defaultView.confirm(TbSync.getString("prompt.Unsubscribe"))) { + folder.targetData.removeTarget(); + folder.setFolderProperty("selected", false); + } else { + if (element) { + //undo users action + element.setAttribute("checked", true); + } + } + } catch (e) { + folder.setFolderProperty("selected", false); + Components.utils.reportError(e); + } + } else { + //select and update status + folder.setFolderProperty("selected", true); + folder.setFolderProperty("status", "aborted"); + folder.accountData.setAccountProperty("status", "notsyncronized"); + } + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folder.accountID); + } + } + + /** + * updateReadOnly event + */ + updateReadOnly(event) { + let element = event.target; + let folderList = element.ownerDocument.getElementById("tbsync.accountsettings.folderlist"); + if (folderList.selectedItem !== null && !folderList.disabled) { + //the folderData obj of the selected folder is attached to its row entry + let folder = folderList.selectedItem.folderData; + + //update value + let value = element.downloadonly; + folder.setFolderProperty("downloadonly", value); + + //update icon + let button = element.parentNode.parentNode; + if (value) { + button.setAttribute('image','chrome://tbsync/content/skin/acl_ro.png'); + } else { + button.setAttribute('image','chrome://tbsync/content/skin/acl_rw.png'); + } + + folder.targetData.setReadOnly(value); + } + } + + /** + * Is called to update a row of the folderlist (the first cell is a select checkbox inserted by TbSync) + * + * @param document [in] document object of the account settings window + * @param listItem [in] the listitem of the row, which needs to be updated + * @param folderData [in] FolderData for that row + */ + updateRow(document, listItem, folderData) { + let foldername = TbSync.providers[this.provider].StandardFolderList.getFolderDisplayName(folderData); + let status = folderData.getFolderStatus(); + let selected = folderData.getFolderProperty("selected"); + + // get updatefields + let fields = {} + for (let f of listItem.querySelectorAll("[updatefield]")) { + fields[f.getAttribute("updatefield")] = f; + } + + // update fields + fields.foldername.setAttribute("disabled", !selected); + fields.foldername.setAttribute("style", selected ? "" : "font-style:italic"); + if (fields.foldername.textContent != foldername) { + fields.foldername.textContent = foldername; + fields.foldername.flex = "1"; + } + + fields.status.setAttribute("style", selected ? "" : "font-style:italic"); + if (fields.status.textContent != status) { + fields.status.textContent = status; + fields.status.flex = "1"; + } + + if (fields.hasOwnProperty("acl")) { + fields.acl.setAttribute("image", "chrome://tbsync/content/skin/acl_" + (folderData.getFolderProperty("downloadonly") ? "ro" : "rw") + ".png"); + fields.acl.setAttribute("disabled", folderData.accountData.isSyncing()); + } + + // update selectbox + let selbox = fields.selectbox; + if (selbox) { + if (folderData.getFolderProperty("selected")) { + selbox.setAttribute("checked", true); + } else { + selbox.removeAttribute("checked"); + } + + if (folderData.accountData.isSyncing()) { + selbox.setAttribute("disabled", true); + } else { + selbox.removeAttribute("disabled"); + } + } + } +} diff --git a/content/modules/messenger.js b/content/modules/messenger.js new file mode 100644 index 0000000..eb986c7 --- /dev/null +++ b/content/modules/messenger.js @@ -0,0 +1,99 @@ +/* + * This file is part of TbSync. + * + * 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 messenger = { + + overlayManager : null, + + load: async function () { + this.overlayManager = new OverlayManager(TbSync.extension, {verbose: 0}); + await this.overlayManager.registerOverlay("chrome://messenger/content/messenger.xhtml", "chrome://tbsync/content/overlays/messenger.xhtml"); + Services.obs.addObserver(this.initSyncObserver, "tbsync.observer.sync", false); + Services.obs.addObserver(this.syncstateObserver, "tbsync.observer.manager.updateSyncstate", false); + + //inject overlays + this.overlayManager.startObserving(); + + }, + + unload: async function () { + //unload overlays + this.overlayManager.stopObserving(); + + Services.obs.removeObserver(this.initSyncObserver, "tbsync.observer.sync"); + Services.obs.removeObserver(this.syncstateObserver, "tbsync.observer.manager.updateSyncstate"); + }, + + // observer to catch changing syncstate and to update the status bar. + syncstateObserver: { + observe: function (aSubject, aTopic, aData) { + //update status bar in all main windows + let windows = Services.wm.getEnumerator("mail:3pane"); + while (windows.hasMoreElements()) { + let domWindow = windows.getNext(); + if (TbSync) { + let status = domWindow.document.getElementById("tbsync.status"); + if (status) { + let label = "TbSync: "; + + if (TbSync.enabled) { + + //check if any account is syncing, if not switch to idle + let accounts = TbSync.db.getAccounts(); + let idle = true; + let err = false; + + for (let i=0; i<accounts.allIDs.length && idle; i++) { + if (!accounts.IDs.includes(accounts.allIDs[i])) { + err = true; + continue; + } + + //set idle to false, if at least one account is syncing + if (TbSync.core.isSyncing(accounts.allIDs[i])) idle = false; + + //check for errors + switch (TbSync.db.getAccountProperty(accounts.allIDs[i], "status")) { + case "success": + case "disabled": + case "notsyncronized": + case "syncing": + break; + default: + err = true; + } + } + + if (idle) { + if (err) label += TbSync.getString("info.error"); + else label += TbSync.getString("info.idle"); + } else { + label += TbSync.getString("status.syncing"); + } + } else { + label += "Loading"; + } + status.value = label; + } + } + } + } + }, + + // observer to init sync + initSyncObserver: { + observe: function (aSubject, aTopic, aData) { + if (TbSync.enabled) { + TbSync.core.syncAllAccounts(); + } else { + //TbSync.manager.popupNotEnabled(); + } + } + }, +} diff --git a/content/modules/network.js b/content/modules/network.js new file mode 100644 index 0000000..f5e81d3 --- /dev/null +++ b/content/modules/network.js @@ -0,0 +1,114 @@ +/* + * This file is part of TbSync. + * + * 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 network = { + + load: async function () { + }, + + unload: async function () { + }, + + getContainerIdForUser: function(username) { + //define the allowed range of container ids to be used + let min = 10000; + let max = 19999; + + //we need to store the container map in the main window, so it is persistent and survives a restart of this bootstrapped addon + //TODO: is there a better way to store this container map globally? Can there be TWO main windows? + let mainWindow = Services.wm.getMostRecentWindow("mail:3pane"); + + //init + if (!(mainWindow._containers)) { + mainWindow._containers = []; + } + + //reset if adding an entry will exceed allowed range + if (mainWindow._containers.length > (max-min) && mainWindow._containers.indexOf(username) == -1) { + for (let i=0; i < mainWindow._containers.length; i++) { + //Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: i + min }); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", JSON.stringify({ userContextId: i + min })); + } + mainWindow._containers = []; + } + + let idx = mainWindow._containers.indexOf(username); + return (idx == -1) ? mainWindow._containers.push(username) - 1 + min : (idx + min); + }, + + resetContainerForUser: function(username) { + let id = this.getContainerIdForUser(username); + Services.obs.notifyObservers(null, "clear-origin-attributes-data", JSON.stringify({ userContextId: id })); + }, + + createTCPErrorFromFailedXHR: function (xhr) { + return this.createTCPErrorFromFailedRequest(xhr.channel.QueryInterface(Components.interfaces.nsIRequest)); + }, + + createTCPErrorFromFailedRequest: function (request) { + //adapted from : + //https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL + //codes: https://developer.mozilla.org/en-US/docs/Mozilla/Errors + let status = request.status; + + if ((status & 0xff0000) === 0x5a0000) { // Security module + const nsINSSErrorsService = Components.interfaces.nsINSSErrorsService; + let nssErrorsService = Components.classes['@mozilla.org/nss_errors_service;1'].getService(nsINSSErrorsService); + + // NSS_SEC errors (happen below the base value because of negative vals) + if ((status & 0xffff) < Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)) { + + // The bases are actually negative, so in our positive numeric space, we + // need to subtract the base off our value. + let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); + switch (nssErr) { + case 11: return 'security::SEC_ERROR_EXPIRED_CERTIFICATE'; + case 12: return 'security::SEC_ERROR_REVOKED_CERTIFICATE'; + case 13: return 'security::SEC_ERROR_UNKNOWN_ISSUER'; + case 20: return 'security::SEC_ERROR_UNTRUSTED_ISSUER'; + case 21: return 'security::SEC_ERROR_UNTRUSTED_CERT'; + case 36: return 'security::SEC_ERROR_CA_CERT_INVALID'; + case 90: return 'security::SEC_ERROR_INADEQUATE_KEY_USAGE'; + case 176: return 'security::SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED'; + } + return 'security::UNKNOWN_SECURITY_ERROR'; + + } else { + + // Calculating the difference + let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); + switch (sslErr) { + case 3: return 'security::SSL_ERROR_NO_CERTIFICATE'; + case 4: return 'security::SSL_ERROR_BAD_CERTIFICATE'; + case 8: return 'security::SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE'; + case 9: return 'security::SSL_ERROR_UNSUPPORTED_VERSION'; + case 12: return 'security::SSL_ERROR_BAD_CERT_DOMAIN'; + } + return 'security::UNKOWN_SSL_ERROR'; + + } + + } else { //not the security module + + switch (status) { + case 0x804B000C: return 'network::NS_ERROR_CONNECTION_REFUSED'; + case 0x804B000E: return 'network::NS_ERROR_NET_TIMEOUT'; + case 0x804B001E: return 'network::NS_ERROR_UNKNOWN_HOST'; + case 0x804B0047: return 'network::NS_ERROR_NET_INTERRUPT'; + case 0x805303F4: return 'network::NS_ERROR_DOM_BAD_URI'; + // Custom error + case 0x804B002F: return 'network::REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP'; + } + return 'network::UNKNOWN_NETWORK_ERROR'; + + } + return null; + }, +} diff --git a/content/modules/passwordManager.js b/content/modules/passwordManager.js new file mode 100644 index 0000000..0145cd1 --- /dev/null +++ b/content/modules/passwordManager.js @@ -0,0 +1,78 @@ +/* + * This file is part of TbSync. + * + * 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 passwordManager = { + + load: async function () { + }, + + unload: async function () { + }, + + removeLoginInfos: function(origin, realm, users = null) { + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Components.interfaces.nsILoginInfo, "init"); + + let logins = Services.logins.findLogins(origin, null, realm); + for (let i = 0; i < logins.length; i++) { + if (!users || users.includes(logins[i].username)) { + let currentLoginInfo = new nsLoginInfo(origin, null, realm, logins[i].username, logins[i].password, "", ""); + try { + Services.logins.removeLogin(currentLoginInfo); + } catch (e) { + TbSync.dump("Error removing loginInfo", e); + } + } + } + }, + + updateLoginInfo: function(origin, realm, oldUser, newUser, newPassword) { + let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Components.interfaces.nsILoginInfo, "init"); + + this.removeLoginInfos(origin, realm, [oldUser, newUser]); + + let newLoginInfo = new nsLoginInfo(origin, null, realm, newUser, newPassword, "", ""); + try { + Services.logins.addLogin(newLoginInfo); + } catch (e) { + TbSync.dump("Error adding loginInfo", e); + } + }, + + getLoginInfo: function(origin, realm, user) { + let logins = Services.logins.findLogins(origin, null, realm); + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == user) { + return logins[i].password; + } + } + return null; + }, + + + /** data obj + windowID + accountName + userName + userNameLocked + + reference is an object in which an entry with windowID will be placed to hold a reference to the prompt window (so it can be closed externaly) + */ + asyncPasswordPrompt: async function(data, reference) { + if (data.windowID) { + let url = "chrome://tbsync/content/passwordPrompt/passwordPrompt.xhtml"; + + return await new Promise(function(resolve, reject) { + reference[data.windowID] = TbSync.window.openDialog(url, "TbSyncPasswordPrompt:" + data.windowID, "centerscreen,chrome,resizable=no", data, resolve); + }); + } + + return false; + } +} diff --git a/content/modules/providers.js b/content/modules/providers.js new file mode 100644 index 0000000..562b763 --- /dev/null +++ b/content/modules/providers.js @@ -0,0 +1,184 @@ +/* + * This file is part of TbSync. + * + * 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 providers = { + + //list of default providers (available in add menu, even if not installed) + defaultProviders: { + "google" : { + name: "Google's People API", + homepageUrl: "https://addons.thunderbird.net/addon/google-4-tbsync/"}, + "dav" : { + name: "CalDAV & CardDAV", + homepageUrl: "https://addons.thunderbird.net/addon/dav-4-tbsync/"}, + "eas" : { + name: "Exchange ActiveSync", + homepageUrl: "https://addons.thunderbird.net/addon/eas-4-tbsync/"}, + }, + + loadedProviders: null, + + load: async function () { + this.loadedProviders = {}; + }, + + unload: async function () { + for (let provider in this.loadedProviders) { + await this.unloadProvider(provider); + } + }, + + + + + + loadProvider: async function (extension, provider, js) { + //only load, if not yet loaded and if the provider name does not shadow a fuction inside provider.js + if (!this.loadedProviders.hasOwnProperty(provider) && !this.hasOwnProperty(provider) && js.startsWith("chrome://")) { + try { + let addon = await AddonManager.getAddonByID(extension.id); + + //load provider subscripts into TbSync + this[provider] = {}; + Services.scriptloader.loadSubScript(js, this[provider], "UTF-8"); + if (TbSync.apiVersion != this[provider].Base.getApiVersion()) { + throw new Error("API version mismatch, TbSync@"+TbSync.apiVersion+" vs " + provider + "@" + this[provider].Base.getApiVersion()); + } + + this.loadedProviders[provider] = { + addon, extension, + addonId: extension.id, + version: addon.version.toString(), + createAccountWindow: null + }; + + addon.contributorsURL = this[provider].Base.getContributorsUrl(); + + // check if provider has its own implementation of folderList + if (!this[provider].hasOwnProperty("folderList")) this[provider].folderList = new TbSync.manager.FolderList(provider); + + //load provider + await this[provider].Base.load(); + + await TbSync.messenger.overlayManager.registerOverlay("chrome://tbsync/content/manager/editAccount.xhtml?provider=" + provider, this[provider].Base.getEditAccountOverlayUrl()); + TbSync.dump("Loaded provider", provider + "::" + this[provider].Base.getProviderName() + " ("+this.loadedProviders[provider].version+")"); + + // reset all accounts of this provider + let providerData = new TbSync.ProviderData(provider); + let accounts = providerData.getAllAccounts(); + for (let accountData of accounts) { + // reset sync objects + TbSync.core.resetSyncDataObj(accountData.accountID); + + // set all accounts which are syncing to notsyncronized + if (accountData.getAccountProperty("status") == "syncing") accountData.setAccountProperty("status", "notsyncronized"); + + // set each folder with PENDING status to ABORTED + let folders = TbSync.db.findFolders({"status": "pending"}, {"accountID": accountData.accountID}); + + for (let f=0; f < folders.length; f++) { + TbSync.db.setFolderProperty(folders[f].accountID, folders[f].folderID, "status", "aborted"); + } + } + + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateProviderList", provider); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null); + + // TB60 -> TB68 migration - remove icon and rename target if stale + for (let addressBook of MailServices.ab.directories) { + if (addressBook instanceof Components.interfaces.nsIAbDirectory) { + let storedProvider = TbSync.addressbook.getStringValue(addressBook, "tbSyncProvider", ""); + if (provider == storedProvider && providerData.getFolders({"target": addressBook.UID}).length == 0) { + let name = addressBook.dirName; + addressBook.dirName = TbSync.getString("target.orphaned") + ": " + name; + addressBook.setStringValue("tbSyncIcon", "orphaned"); + addressBook.setStringValue("tbSyncProvider", "orphaned"); + addressBook.setStringValue("tbSyncAccountID", ""); + } + } + } + + let calManager = TbSync.lightning.cal.manager; + for (let calendar of calManager.getCalendars({})) { + let storedProvider = calendar.getProperty("tbSyncProvider"); + if (provider == storedProvider && calendar.type == "storage" && providerData.getFolders({"target": calendar.id}).length == 0) { + let name = calendar.name; + calendar.name = TbSync.getString("target.orphaned") + ": " + name; + calendar.setProperty("disabled", true); + calendar.setProperty("tbSyncProvider", "orphaned"); + calendar.setProperty("tbSyncAccountID", ""); + } + } + + } catch (e) { + delete this.loadedProviders[provider]; + delete this[provider]; + let info = new EventLogInfo(provider); + TbSync.eventlog.add("error", info, "FAILED to load provider <"+provider+">", e.message); + Components.utils.reportError(e); + } + + } + }, + + unloadProvider: async function (provider) { + if (this.loadedProviders.hasOwnProperty(provider)) { + TbSync.dump("Unloading provider", provider); + + if (this.loadedProviders[provider].createAccountWindow) { + this.loadedProviders[provider].createAccountWindow.close(); + } + + await this[provider].Base.unload(); + delete this.loadedProviders[provider]; + delete this[provider]; + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateProviderList", provider); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null); + } + }, + + getDefaultAccountEntries: function (provider) { + let defaults = TbSync.providers[provider].Base.getDefaultAccountEntries(); + + // List of default system account properties. + // Do not remove search marker for doc. + // DefaultAccountPropsStart + defaults.provider = provider; + defaults.accountID = ""; + defaults.lastsynctime = 0; + defaults.status = "disabled"; + defaults.autosync = 0; + defaults.noAutosyncUntil = 0; + defaults.accountname = ""; + // DefaultAccountPropsEnd + + return defaults; + }, + + getDefaultFolderEntries: function (accountID) { + let provider = TbSync.db.getAccountProperty(accountID, "provider"); + let defaults = TbSync.providers[provider].Base.getDefaultFolderEntries(); + + // List of default system folder properties. + // Do not remove search marker for doc. + // DefaultFolderPropsStart + defaults.accountID = accountID; + defaults.targetType = ""; + defaults.cached = false; + defaults.selected = false; + defaults.lastsynctime = 0; + defaults.status = ""; + defaults.foldername = ""; + defaults.downloadonly = false; + // DefaultFolderPropsEnd + + return defaults; + }, +} diff --git a/content/modules/public.js b/content/modules/public.js new file mode 100644 index 0000000..afd4f03 --- /dev/null +++ b/content/modules/public.js @@ -0,0 +1,757 @@ +/* + * This file is part of TbSync. + * + * 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 StatusData = class { + /** + * A StatusData instance must be used as return value by + * :class:`Base.syncFolderList` and :class:`Base.syncFolder`. + * + * StatusData also defines the possible StatusDataTypes used by the + * :ref:`TbSyncEventLog`. + * + * @param {StatusDataType} type Status type (see const definitions below) + * @param {string} message ``Optional`` A message, which will be used as + * sync status. If this is not a success, it will be + * used also in the :ref:`TbSyncEventLog` as well. + * @param {string} details ``Optional`` If this is not a success, it will + * be used as description in the + * :ref:`TbSyncEventLog`. + * + */ + constructor(type = "success", message = "", details = "") { + this.type = type; //success, info, warning, error + this.message = message; + this.details = details; + } + /** + * Successfull sync. + */ + static get SUCCESS() {return "success"}; + /** + * Sync of the entire account will be aborted. + */ + static get ERROR() {return "error"}; + /** + * Sync of this resource will be aborted and continued with next resource. + */ + static get WARNING() {return "warning"}; + /** + * Successfull sync, but message and details + * provided will be added to the event log. + */ + static get INFO() {return "info"}; + /** + * Sync of the entire account will be aborted and restarted completely. + */ + static get ACCOUNT_RERUN() {return "account_rerun"}; + /** + * Sync of the current folder/resource will be restarted. + */ + static get FOLDER_RERUN() {return "folder_rerun"}; +} + + + +/** + * ProgressData to manage a ``done`` and a ``todo`` counter. + * + * Each :class:`SyncData` instance has an associated ProgressData instance. See + * :class:`SyncData.progressData`. The information of that ProgressData + * instance is used, when the current syncstate is prefixed by ``send.``, + * ``eval.`` or ``prepare.``. See :class:`SyncData.setSyncState`. + * + */ +var ProgressData = class { + /** + * + */ + constructor() { + this._todo = 0; + this._done = 0; + } + + /** + * Reset ``done`` and ``todo`` counter. + * + * @param {integer} done ``Optional`` Set a value for the ``done`` counter. + * @param {integer} todo ``Optional`` Set a value for the ``todo`` counter. + * + */ + reset(done = 0, todo = 0) { + this._todo = todo; + this._done = done; + } + + /** + * Increment the ``done`` counter. + * + * @param {integer} value ``Optional`` Set incrementation value. + * + */ + inc(value = 1) { + this._done += value; + } + + /** + * Getter for the ``todo`` counter. + * + */ + get todo() { + return this._todo; + } + + /** + * Getter for the ``done`` counter. + * + */ + get done() { + return this._done; + } +} + + + +/** + * ProviderData + * + */ +var ProviderData = class { + /** + * Constructor + * + * @param {FolderData} folderData FolderData of the folder for which the + * display name is requested. + * + */ + constructor(provider) { + if (!TbSync.providers.hasOwnProperty(provider)) { + throw new Error("Provider <" + provider + "> has not been loaded. Failed to create ProviderData."); + } + this.provider = provider; + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this ProviderData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.getAccountProperty("provider")); + } + + getVersion() { + return TbSync.providers.loadedProviders[this.provider].version; + } + + get extension() { + return TbSync.providers.loadedProviders[this.provider].extension; + } + + getAllAccounts() { + let accounts = TbSync.db.getAccounts(); + let allAccounts = []; + for (let i=0; i<accounts.IDs.length; i++) { + let accountID = accounts.IDs[i]; + if (accounts.data[accountID].provider == this.provider) { + allAccounts.push(new TbSync.AccountData(accountID)); + } + } + return allAccounts; + } + + getFolders(aFolderSearchCriteria = {}) { + let allFolders = []; + let folderSearchCriteria = {}; + Object.assign(folderSearchCriteria, aFolderSearchCriteria); + folderSearchCriteria.cached = false; + + let folders = TbSync.db.findFolders(folderSearchCriteria, {"provider": this.provider}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(new TbSync.AccountData(folders[i].accountID), folders[i].folderID)); + } + return allFolders; + } + + getDefaultAccountEntries() { + return TbSync.providers.getDefaultAccountEntries(this.provider) + } + + addAccount(accountName, accountOptions) { + let newAccountID = TbSync.db.addAccount(accountName, accountOptions); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountsList", newAccountID); + return new TbSync.AccountData(newAccountID); + } +} + + + +/** + * AccountData + * + */ +var AccountData = class { + /** + * + */ + constructor(accountID) { + this._accountID = accountID; + + if (!TbSync.db.accounts.data.hasOwnProperty(accountID)) { + throw new Error("An account with ID <" + accountID + "> does not exist. Failed to create AccountData."); + } + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this AccountData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.getAccountProperty("provider"), + this.getAccountProperty("accountname"), + this.accountID); + } + + get accountID() { + return this._accountID; + } + + getAllFolders() { + let allFolders = []; + let folders = TbSync.db.findFolders({"cached": false}, {"accountID": this.accountID}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(this, folders[i].folderID)); + } + return allFolders; + } + + getAllFoldersIncludingCache() { + let allFolders = []; + let folders = TbSync.db.findFolders({}, {"accountID": this.accountID}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(this, folders[i].folderID)); + } + return allFolders; + } + + getFolder(setting, value) { + // ES6 supports variable keys by putting it into brackets + let folders = TbSync.db.findFolders({[setting]: value, "cached": false}, {"accountID": this.accountID}); + if (folders.length > 0) return new TbSync.FolderData(this, folders[0].folderID); + return null; + } + + getFolderFromCache(setting, value) { + // ES6 supports variable keys by putting it into brackets + let folders = TbSync.db.findFolders({[setting]: value, "cached": true}, {"accountID": this.accountID}); + if (folders.length > 0) return new TbSync.FolderData(this, folders[0].folderID); + return null; + } + + createNewFolder() { + return new TbSync.FolderData(this, TbSync.db.addFolder(this.accountID)); + } + + // get data objects + get providerData() { + return new TbSync.ProviderData( + this.getAccountProperty("provider"), + ); + } + + get syncData() { + return TbSync.core.getSyncDataObject(this.accountID); + } + + + /** + * Initiate a sync of this entire account by calling + * :class:`Base.syncFolderList`. If that succeeded, :class:`Base.syncFolder` + * will be called for each available folder / resource found on the server. + * + * @param {Object} syncDescription ``Optional`` + */ + sync(syncDescription = {}) { + TbSync.core.syncAccount(this.accountID, syncDescription); + } + + isSyncing() { + return TbSync.core.isSyncing(this.accountID); + } + + isEnabled() { + return TbSync.core.isEnabled(this.accountID); + } + + isConnected() { + return TbSync.core.isConnected(this.accountID); + } + + + getAccountProperty(field) { + return TbSync.db.getAccountProperty(this.accountID, field); + } + + setAccountProperty(field, value) { + TbSync.db.setAccountProperty(this.accountID, field, value); + Services.obs.notifyObservers(null, "tbsync.observer.manager.reloadAccountSetting", JSON.stringify({accountID: this.accountID, setting: field})); + } + + resetAccountProperty(field) { + TbSync.db.resetAccountProperty(this.accountID, field); + Services.obs.notifyObservers(null, "tbsync.observer.manager.reloadAccountSetting", JSON.stringify({accountID: this.accountID, setting: field})); + } +} + + + +/** + * FolderData + * + */ +var FolderData = class { + /** + * + */ + constructor(accountData, folderID) { + this._accountData = accountData; + this._folderID = folderID; + this._target = null; + + if (!TbSync.db.folders[accountData.accountID].hasOwnProperty(folderID)) { + throw new Error("A folder with ID <" + folderID + "> does not exist for the given account. Failed to create FolderData."); + } + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this FolderData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.accountData.getAccountProperty("provider"), + this.accountData.getAccountProperty("accountname"), + this.accountData.accountID, + this.getFolderProperty("foldername"), + ); + } + + get folderID() { + return this._folderID; + } + + get accountID() { + return this._accountData.accountID; + } + + getDefaultFolderEntries() { // remove + return TbSync.providers.getDefaultFolderEntries(this.accountID); + } + + getFolderProperty(field) { + return TbSync.db.getFolderProperty(this.accountID, this.folderID, field); + } + + setFolderProperty(field, value) { + TbSync.db.setFolderProperty(this.accountID, this.folderID, field, value); + } + + resetFolderProperty(field) { + TbSync.db.resetFolderProperty(this.accountID, this.folderID, field); + } + + /** + * Initiate a sync of this folder only by calling + * :class:`Base.syncFolderList` and than :class:`Base.syncFolder` for this + * folder / resource only. + * + * @param {Object} syncDescription ``Optional`` + */ + sync(aSyncDescription = {}) { + let syncDescription = {}; + Object.assign(syncDescription, aSyncDescription); + + syncDescription.syncFolders = [this]; + this.accountData.sync(syncDescription); + } + + isSyncing() { + let syncdata = this.accountData.syncData; + return (syncdata.currentFolderData && syncdata.currentFolderData.folderID == this.folderID); + } + + getFolderStatus() { + let status = ""; + + if (this.getFolderProperty("selected")) { + //default + status = TbSync.getString("status." + this.getFolderProperty("status"), this.accountData.getAccountProperty("provider")).split("||")[0]; + + switch (this.getFolderProperty("status").split(".")[0]) { //the status may have a sub-decleration + case "modified": + //trigger periodic sync (TbSync.syncTimer, tbsync.jsm) + if (!this.isSyncing()) { + this.accountData.setAccountProperty("lastsynctime", 0); + } + case "success": + try { + status = status + ": " + this.targetData.targetName; + } catch (e) { + this.resetFolderProperty("target"); + this.setFolderProperty("status","notsyncronized"); + return TbSync.getString("status.notsyncronized"); + } + break; + + case "pending": + //add extra info if this folder is beeing synced + if (this.isSyncing()) { + let syncdata = this.accountData.syncData; + status = TbSync.getString("status.syncing", this.accountData.getAccountProperty("provider")); + if (["send","eval","prepare"].includes(syncdata.getSyncState().state.split(".")[0]) && (syncdata.progressData.todo + syncdata.progressData.done) > 0) { + //add progress information + status = status + " (" + syncdata.progressData.done + (syncdata.progressData.todo > 0 ? "/" + syncdata.progressData.todo : "") + ")"; + } + } + break; + } + } else { + //remain empty if not selected + } + return status; + } + + // get data objects + get accountData() { + return this._accountData; + } + + /** + * Getter for the :class:`TargetData` instance associated with this + * FolderData. See :ref:`TbSyncTargets` for more details. + * + * @returns {TargetData} + * + */ + get targetData() { + // targetData is created on demand + if (!this._target) { + let provider = this.accountData.getAccountProperty("provider"); + let targetType = this.getFolderProperty("targetType"); + + if (!targetType) + throw new Error("Provider <"+provider+"> has not set a proper target type for this folder."); + + if (!TbSync.providers[provider].hasOwnProperty("TargetData_" + targetType)) + throw new Error("Provider <"+provider+"> is missing a TargetData implementation for <"+targetType+">."); + + this._target = new TbSync.providers[provider]["TargetData_" + targetType](this); + + if (!this._target) + throw new Error("notargets"); + } + + return this._target; + } + + // Removes the folder and its target. If the target should be + // kept as a stale/unconnected item, provide a suffix, which + // will be added to its name, to indicate, that it is no longer + // managed by TbSync. + remove(keepStaleTargetSuffix = "") { + // hasTarget() can throw an error, ignore that here + try { + if (this.targetData.hasTarget()) { + if (keepStaleTargetSuffix) { + let oldName = this.targetData.targetName; + this.targetData.targetName = TbSync.getString("target.orphaned") + ": " + oldName + " " + keepStaleTargetSuffix; + this.targetData.disconnectTarget(); + } else { + this.targetData.removeTarget(); + } + } + } catch (e) { + Components.utils.reportError(e); + } + this.setFolderProperty("cached", true); + } +} + + + +/** + * There is only one SyncData instance per account which contains all + * relevant information regarding an ongoing sync. + * + */ +var SyncData = class { + /** + * + */ + constructor(accountID) { + + //internal (private, not to be touched by provider) + this._syncstate = { + state: "accountdone", + timestamp: Date.now(), + } + this._accountData = new TbSync.AccountData(accountID); + this._progressData = new TbSync.ProgressData(); + this._currentFolderData = null; + } + + //all functions provider should use should be in here + //providers should not modify properties directly + //when getSyncDataObj is used never change the folder id as a sync may be going on! + + _setCurrentFolderData(folderData) { + this._currentFolderData = folderData; + } + _clearCurrentFolderData() { + this._currentFolderData = null; + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this SyncData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.accountData.getAccountProperty("provider"), + this.accountData.getAccountProperty("accountname"), + this.accountData.accountID, + this.currentFolderData ? this.currentFolderData.getFolderProperty("foldername") : "", + ); + } + + /** + * Getter for the :class:`FolderData` instance of the folder being currently + * synced. Can be ``null`` if no folder is being synced. + * + */ + get currentFolderData() { + return this._currentFolderData; + } + + /** + * Getter for the :class:`AccountData` instance of the account being + * currently synced. + * + */ + get accountData() { + return this._accountData; + } + + /** + * Getter for the :class:`ProgressData` instance of the ongoing sync. + * + */ + get progressData() { + return this._progressData; + } + + /** + * Sets the syncstate of the ongoing sync, to provide feedback to the user. + * The selected state can trigger special UI features, if it starts with one + * of the following prefixes: + * + * * ``send.``, ``eval.``, ``prepare.`` : + * The status message in the UI will be appended with the current progress + * stored in the :class:`ProgressData` associated with this SyncData + * instance. See :class:`SyncData.progressData`. + * + * * ``send.`` : + * The status message in the UI will be appended by a timeout countdown + * with the timeout being defined by :class:`Base.getConnectionTimeout`. + * + * @param {string} state A short syncstate identifier. The actual + * message to be displayed in the UI will be + * looked up in the locales of the provider + * by looking for ``syncstate.<state>``. + * The lookup is done via :func:`getString`, + * so the same fallback rules apply. + * + */ + setSyncState(state) { + //set new syncstate + let msg = "State: " + state + ", Account: " + this.accountData.getAccountProperty("accountname"); + if (this.currentFolderData) msg += ", Folder: " + this.currentFolderData.getFolderProperty("foldername"); + + let syncstate = {}; + syncstate.state = state; + syncstate.timestamp = Date.now(); + + this._syncstate = syncstate; + TbSync.dump("setSyncState", msg); + + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", this.accountData.accountID); + } + + /** + * Gets the current syncstate and its timestamp of the ongoing sync. The + * returned Object has the following attributes: + * + * * ``state`` : the current syncstate + * * ``timestamp`` : its timestamp + * + * @returns {Object} The syncstate and its timestamp. + * + */ + getSyncState() { + return this._syncstate; + } +} + + + + + + + + + + +// Simple dumper, who can dump to file or console +// It is suggested to use the event log instead of dumping directly. +var dump = function (what, aMessage) { + if (TbSync.prefs.getBoolPref("log.toconsole")) { + Services.console.logStringMessage("[TbSync] " + what + " : " + aMessage); + } + + if (TbSync.prefs.getIntPref("log.userdatalevel") > 0) { + let now = new Date(); + TbSync.io.appendToFile("debug.log", "** " + now.toString() + " **\n[" + what + "] : " + aMessage + "\n\n"); + } +} + + + +/** + * Get a localized string. + * + * TODO: Explain placeholder and :: notation. + * + * @param {string} key The key of the message to look up + * @param {string} provider ``Optional`` The provider the key belongs to. + * + * @returns {string} The message belonging to the key of the specified provider. + * If that key is not found in the in the specified provider + * or if no provider has been specified, the messages of + * TbSync itself we be used as fallback. If the key could not + * be found there as well, the key itself is returned. + * + */ +var getString = function (key, provider) { + let localized = null; + + //spezial treatment of strings with :: like status.httperror::403 + let parts = key.split("::"); + + // if a provider is given, try to get the string from the provider + if (provider && TbSync.providers.loadedProviders.hasOwnProperty(provider)) { + let localeData = TbSync.providers.loadedProviders[provider].extension.localeData; + if (localeData.messages.get(localeData.selectedLocale).has(parts[0].toLowerCase())) { + localized = TbSync.providers.loadedProviders[provider].extension.localeData.localizeMessage(parts[0]); + } + } + + // if we did not yet succeed, check the locales of tbsync itself + if (!localized) { + localized = TbSync.extension.localeData.localizeMessage(parts[0]); + } + + if (!localized) { + localized = key; + } else { + //replace placeholders in returned string + for (let i = 0; i<parts.length; i++) { + let regex = new RegExp( "##replace\."+i+"##", "g"); + localized = localized.replace(regex, parts[i]); + } + } + + return localized; +} + + +var localizeNow = function (window, provider) { + let document = window.document; + let keyPrefix = "__" + (provider ? provider.toUpperCase() + "4" : "") + "TBSYNCMSG_"; + + let localization = { + i18n: null, + + updateString(string) { + let re = new RegExp(keyPrefix + "(.+?)__", "g"); + return string.replace(re, matched => { + const key = matched.slice(keyPrefix.length, -2); + return TbSync.getString(key, provider) || matched; + }); + }, + + updateDocument(node) { + const texts = document.evaluate( + 'descendant::text()[contains(self::text(), "' + keyPrefix + '")]', + node, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) { + const text = texts.snapshotItem(i); + if (text.nodeValue.includes(keyPrefix)) text.nodeValue = this.updateString(text.nodeValue); + } + + const attributes = document.evaluate( + 'descendant::*/attribute::*[contains(., "' + keyPrefix + '")]', + node, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) { + const attribute = attributes.snapshotItem(i); + if (attribute.value.includes(keyPrefix)) attribute.value = this.updateString(attribute.value); + } + } + }; + + localization.updateDocument(document); +} + +var localizeOnLoad = function (window, provider) { + // standard event if loaded by a standard window + window.document.addEventListener('DOMContentLoaded', () => { + this.localizeNow(window, provider); + }, { once: true }); + + // custom event, fired by the overlay loader after it has finished loading + // the editAccount dialog is never called as a provider, but from tbsync itself + let eventId = "DOMOverlayLoaded_" + + (!provider || window.location.href.startsWith("chrome://tbsync/content/manager/editAccount.") ? "" : provider + "4") + + "tbsync@jobisoft.de"; + window.document.addEventListener(eventId, () => { + TbSync.localizeNow(window, provider); + }, { once: true }); +} + + + +var generateUUID = function () { + const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + return uuidGenerator.generateUUID().toString().replace(/[{}]/g, ''); +} diff --git a/content/modules/tools.js b/content/modules/tools.js new file mode 100644 index 0000000..fe406b9 --- /dev/null +++ b/content/modules/tools.js @@ -0,0 +1,80 @@ +/* + * This file is part of TbSync. + * + * 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 tools = { + + load: async function () { + }, + + unload: async function () { + }, + + // async sleep function using Promise to postpone actions to keep UI responsive + sleep : function (_delay, useRequestIdleCallback = false) { + let useIdleCallback = false; + let delay = 5;//_delay; + if (TbSync.window.requestIdleCallback && useRequestIdleCallback) { + useIdleCallback = true; + delay= 2; + } + let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + + return new Promise(function(resolve, reject) { + let event = { + notify: function(timer) { + if (useIdleCallback) { + TbSync.window.requestIdleCallback(resolve); + } else { + resolve(); + } + } + } + timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + }); + }, + + // this is derived from: http://jonisalonen.com/2012/from-utf-16-to-utf-8-in-javascript/ + // javascript strings are utf16, btoa needs utf8 , so we need to encode + toUTF8: function (str) { + var utf8 = ""; + for (var i=0; i < str.length; i++) { + var charcode = str.charCodeAt(i); + if (charcode < 0x80) utf8 += String.fromCharCode(charcode); + else if (charcode < 0x800) { + utf8 += String.fromCharCode(0xc0 | (charcode >> 6), + 0x80 | (charcode & 0x3f)); + } + else if (charcode < 0xd800 || charcode >= 0xe000) { + utf8 += String.fromCharCode(0xe0 | (charcode >> 12), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + + // surrogate pair + else { + i++; + // UTF-16 encodes 0x10000-0x10FFFF by + // subtracting 0x10000 and splitting the + // 20 bits of 0x0-0xFFFFF into two halves + charcode = 0x10000 + (((charcode & 0x3ff)<<10) + | (str.charCodeAt(i) & 0x3ff)) + utf8 += String.fromCharCode(0xf0 | (charcode >>18), + 0x80 | ((charcode>>12) & 0x3f), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + } + return utf8; + }, + + b64encode: function (str) { + return btoa(this.toUTF8(str)); + } +} |