From 9355e23a909a7801b3ccdf68ee05b3480be42407 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 08:29:37 +0200 Subject: Adding upstream version 4.7. Signed-off-by: Daniel Baumann --- content/modules/lightning.js | 774 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 774 insertions(+) create mode 100644 content/modules/lightning.js (limited to 'content/modules/lightning.js') 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; + } +} -- cgit v1.2.3