diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/src/CalCalendarManager.jsm | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/calendar/base/src/CalCalendarManager.jsm | 1076 |
1 files changed, 1076 insertions, 0 deletions
diff --git a/comm/calendar/base/src/CalCalendarManager.jsm b/comm/calendar/base/src/CalCalendarManager.jsm new file mode 100644 index 0000000000..117737a635 --- /dev/null +++ b/comm/calendar/base/src/CalCalendarManager.jsm @@ -0,0 +1,1076 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from calCachedCalendar.js */ + +var EXPORTED_SYMBOLS = ["CalCalendarManager"]; + +const { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs"); +const { Preferences } = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs"); +const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +const { calCachedCalendar } = ChromeUtils.import("resource:///components/calCachedCalendar.js"); +const { setTimeout } = ChromeUtils.importESModule("resource://gre/modules/Timer.sys.mjs"); + +var REGISTRY_BRANCH = "calendar.registry."; +var MAX_INT = Math.pow(2, 31) - 1; +var MIN_INT = -MAX_INT; + +function CalCalendarManager() { + this.wrappedJSObject = this; + this.mObservers = new cal.data.ListenerSet(Ci.calICalendarManagerObserver); + this.mCalendarObservers = new cal.data.ListenerSet(Ci.calIObserver); + + this.providerImplementations = {}; +} + +var calCalendarManagerClassID = Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}"); +var calCalendarManagerInterfaces = [Ci.calICalendarManager, Ci.calIStartupService, Ci.nsIObserver]; +CalCalendarManager.prototype = { + classID: calCalendarManagerClassID, + QueryInterface: cal.generateQI(["calICalendarManager", "calIStartupService", "nsIObserver"]), + classInfo: cal.generateCI({ + classID: calCalendarManagerClassID, + contractID: "@mozilla.org/calendar/manager;1", + classDescription: "Calendar Manager", + interfaces: calCalendarManagerInterfaces, + flags: Ci.nsIClassInfo.SINGLETON, + }), + + get networkCalendarCount() { + return this.mNetworkCalendarCount; + }, + get readOnlyCalendarCount() { + return this.mReadonlyCalendarCount; + }, + get calendarCount() { + return this.mCalendarCount; + }, + + // calIStartupService: + startup(aCompleteListener) { + AddonManager.addAddonListener(gCalendarManagerAddonListener); + this.mCache = null; + this.mCalObservers = null; + this.mRefreshTimer = {}; + this.setupOfflineObservers(); + this.mNetworkCalendarCount = 0; + this.mReadonlyCalendarCount = 0; + this.mCalendarCount = 0; + + // We only add the observer if the pref is set and only check for the + // pref on startup to avoid checking for every http request + if (Services.prefs.getBoolPref("calendar.network.multirealm", false)) { + Services.obs.addObserver(this, "http-on-examine-response"); + } + + aCompleteListener.onResult(null, Cr.NS_OK); + }, + + shutdown(aCompleteListener) { + for (let id in this.mCache) { + let calendar = this.mCache[id]; + calendar.removeObserver(this.mCalObservers[calendar.id]); + } + + this.cleanupOfflineObservers(); + + AddonManager.removeAddonListener(gCalendarManagerAddonListener); + + // Remove the observer if the pref is set. This might fail when the + // user flips the pref, but we assume he is going to restart anyway + // afterwards. + if (Services.prefs.getBoolPref("calendar.network.multirealm", false)) { + Services.obs.removeObserver(this, "http-on-examine-response"); + } + + aCompleteListener.onResult(null, Cr.NS_OK); + }, + + setupOfflineObservers() { + Services.obs.addObserver(this, "network:offline-status-changed"); + }, + + cleanupOfflineObservers() { + Services.obs.removeObserver(this, "network:offline-status-changed"); + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "timer-callback": { + // Refresh all the calendars that can be refreshed. + for (let calendar of this.getCalendars()) { + maybeRefreshCalendar(calendar); + } + break; + } + case "network:offline-status-changed": { + for (let id in this.mCache) { + let calendar = this.mCache[id]; + if (calendar instanceof calCachedCalendar) { + calendar.onOfflineStatusChanged(aData == "offline"); + } + } + break; + } + case "http-on-examine-response": { + try { + let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); + if (channel.notificationCallbacks) { + // We use the notification callbacks to get the calendar interface, which likely works + // for our requests since getInterface is called from the calendar provider context. + let authHeader = channel.getResponseHeader("WWW-Authenticate"); + let calendar = channel.notificationCallbacks.getInterface(Ci.calICalendar); + if (calendar && !calendar.getProperty("capabilities.realmrewrite.disabled")) { + // The provider may choose to explicitly disable the rewriting, for example if all + // calendars on a domain have the same credentials + let escapedName = calendar.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + authHeader = appendToRealm(authHeader, "(" + escapedName + ")"); + channel.setResponseHeader("WWW-Authenticate", authHeader, false); + } + } + } catch (e) { + if (e.result != Cr.NS_NOINTERFACE && e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + // Possible reasons we got here: + // - Its not a http channel (wtf? Oh well) + // - The owner is not a calICalendar (looks like its not our deal) + // - The WWW-Authenticate header is missing (that's ok) + } + break; + } + } + }, + + /** + * calICalendarManager interface + */ + createCalendar(type, uri) { + try { + let calendar; + if (Cc["@mozilla.org/calendar/calendar;1?type=" + type]) { + calendar = Cc["@mozilla.org/calendar/calendar;1?type=" + type].createInstance( + Ci.calICalendar + ); + } else if (this.providerImplementations[type]) { + let CalendarProvider = this.providerImplementations[type]; + calendar = new CalendarProvider(); + if (calendar.QueryInterface) { + calendar = calendar.QueryInterface(Ci.calICalendar); + } + } else { + // Don't notify the user with an extra dialog if the provider interface is missing. + return null; + } + + calendar.uri = uri; + return calendar; + } catch (ex) { + let rc = ex; + if (ex instanceof Ci.nsIException) { + rc = ex.result; + } + + let uiMessage = cal.l10n.getCalString("unableToCreateProvider", [uri.spec]); + + // Log the original exception via error console to provide more debug info + cal.ERROR(ex); + + // Log the possibly translated message via the UI. + let paramBlock = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance( + Ci.nsIDialogParamBlock + ); + paramBlock.SetNumberStrings(3); + paramBlock.SetString(0, uiMessage); + paramBlock.SetString(1, "0x" + rc.toString(0x10)); + paramBlock.SetString(2, ex); + Services.ww.openWindow( + null, + "chrome://calendar/content/calendar-error-prompt.xhtml", + "_blank", + "chrome,dialog=yes,alwaysRaised=yes", + paramBlock + ); + return null; + } + }, + + /** + * Creates a calendar and takes care of initial setup, including enabled/disabled properties and + * cached calendars. If the provider doesn't exist, returns a dummy calendar that is + * force-disabled. + * + * @param {string} id - The calendar id. + * @param {string} ctype - The calendar type. See {@link calICalendar#type}. + * @param {string} uri - The calendar uri. + * @returns {calICalendar} The initialized calendar or dummy calendar. + */ + initializeCalendar(id, ctype, uri) { + let calendar = this.createCalendar(ctype, uri); + if (calendar) { + calendar.id = id; + if (calendar.getProperty("auto-enabled")) { + calendar.deleteProperty("disabled"); + calendar.deleteProperty("auto-enabled"); + } + + calendar = maybeWrapCachedCalendar(calendar); + } else { + // Create dummy calendar that stays disabled for this run. + calendar = new calDummyCalendar(ctype); + calendar.id = id; + calendar.uri = uri; + // Try to enable on next startup if calendar has been enabled. + if (!calendar.getProperty("disabled")) { + calendar.setProperty("auto-enabled", true); + } + calendar.setProperty("disabled", true); + } + + return calendar; + }, + + /** + * Update calendar registrations for the given type. If the provider is missing then the calendars + * are replaced with a dummy calendar, and vice versa. + * + * @param {string} type - The calendar type to update. See {@link calICalendar#type}. + * @param {boolean} [clearCache=false] - If true, the calendar cache is also cleared. + */ + updateDummyCalendarRegistration(type, clearCache = false) { + let hasImplementation = !!this.providerImplementations[type]; + + let calendars = Object.values(this.mCache).filter(calendar => { + // Calendars backed by providers despite missing provider implementation, or dummy calendars + // despite having a provider implementation. + let isDummyCalendar = calendar instanceof calDummyCalendar; + return calendar.type == type && hasImplementation == isDummyCalendar; + }); + this.updateCalendarRegistration(calendars, clearCache); + }, + + /** + * Update the calendar registrations for the given set of calendars. This essentially unregisters + * the calendar, then sets it up again using id, type and uri. This is similar to what happens on + * startup. + * + * @param {calICalendar[]} calendars - The calendars to update. + * @param {boolean} [clearCache=false] - If true, the calendar cache is also cleared. + */ + updateCalendarRegistration(calendars, clearCache = false) { + let sortOrderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "").split(" "); + let sortOrder = {}; + for (let i = 0; i < sortOrderPref.length; i++) { + sortOrder[sortOrderPref[i]] = i; + } + + let needsRefresh = []; + for (let calendar of calendars) { + try { + this.notifyObservers("onCalendarUnregistering", [calendar]); + this.unsetupCalendar(calendar, clearCache); + + let replacement = this.initializeCalendar(calendar.id, calendar.type, calendar.uri); + replacement.setProperty("initialSortOrderPos", sortOrder[calendar.id]); + + this.setupCalendar(replacement); + needsRefresh.push(replacement); + } catch (e) { + cal.ERROR( + `Can't create calendar for ${calendar.id} (${calendar.type}, ${calendar.uri.spec}): ${e}` + ); + } + } + + // Do this in a second pass so that all provider calendars are available. + for (let calendar of needsRefresh) { + maybeRefreshCalendar(calendar); + this.notifyObservers("onCalendarRegistered", [calendar]); + } + }, + + /** + * Register a calendar provider with the given JavaScript implementation. + * + * @param {string} type - The calendar type string, see {@link calICalendar#type}. + * @param {object} impl - The class that implements calICalendar. + */ + registerCalendarProvider(type, impl) { + this.assureCache(); + + cal.ASSERT( + !this.providerImplementations.hasOwnProperty(type), + "[CalCalendarManager::registerCalendarProvider] provider already exists", + true + ); + + this.providerImplementations[type] = impl; + this.updateDummyCalendarRegistration(type); + }, + + /** + * Unregister a calendar provider by type. Already registered calendars will be replaced by a + * dummy calendar that is force-disabled. + * + * @param {string} type - The calendar type string, see {@link calICalendar#type}. + * @param {boolean} temporary - If true, cached calendars will not be cleared. + */ + unregisterCalendarProvider(type, temporary = false) { + cal.ASSERT( + this.providerImplementations.hasOwnProperty(type), + "[CalCalendarManager::unregisterCalendarProvider] provider doesn't exist or is builtin", + true + ); + delete this.providerImplementations[type]; + this.updateDummyCalendarRegistration(type, !temporary); + }, + + /** + * Checks if a calendar provider has been dynamically registered with the given type. This does + * not check for the built-in XPCOM providers. + * + * @param {string} type - The calendar type string, see {@link calICalendar#type}. + * @returns {boolean} True, if the calendar provider type is registered. + */ + hasCalendarProvider(type) { + return !!this.providerImplementations[type]; + }, + + registerCalendar(calendar) { + this.assureCache(); + + // If the calendar is already registered, bail out + cal.ASSERT( + !calendar.id || !(calendar.id in this.mCache), + "[CalCalendarManager::registerCalendar] calendar already registered!", + true + ); + + if (!calendar.id) { + calendar.id = cal.getUUID(); + } + + Services.prefs.setStringPref(getPrefBranchFor(calendar.id) + "type", calendar.type); + Services.prefs.setStringPref(getPrefBranchFor(calendar.id) + "uri", calendar.uri.spec); + + calendar = maybeWrapCachedCalendar(calendar); + + this.setupCalendar(calendar); + flushPrefs(); + + maybeRefreshCalendar(calendar); + this.notifyObservers("onCalendarRegistered", [calendar]); + }, + + /** + * Sets up a calendar, this is the initialization required during calendar registration. See + * {@link #unsetupCalendar} to revert these steps. + * + * @param {calICalendar} calendar - The calendar to set up. + */ + setupCalendar(calendar) { + this.mCache[calendar.id] = calendar; + + // Add an observer to track readonly-mode triggers + let newObserver = new calMgrCalendarObserver(calendar, this); + calendar.addObserver(newObserver); + this.mCalObservers[calendar.id] = newObserver; + + // Set up statistics + if (calendar.getProperty("requiresNetwork") !== false) { + this.mNetworkCalendarCount++; + } + if (calendar.readOnly) { + this.mReadonlyCalendarCount++; + } + this.mCalendarCount++; + + // Set up the refresh timer + this.setupRefreshTimer(calendar); + }, + + /** + * Reverts the calendar registration setup steps from {@link #setupCalendar}. + * + * @param {calICalendar} calendar - The calendar to undo setup for. + * @param {boolean} [clearCache=false] - If true, the cache is cleared for this calendar. + */ + unsetupCalendar(calendar, clearCache = false) { + if (this.mCache) { + delete this.mCache[calendar.id]; + } + + if (clearCache && calendar.wrappedJSObject instanceof calCachedCalendar) { + calendar.wrappedJSObject.onCalendarUnregistering(); + } + + calendar.removeObserver(this.mCalObservers[calendar.id]); + + if (calendar.readOnly) { + this.mReadonlyCalendarCount--; + } + + if (calendar.getProperty("requiresNetwork") !== false) { + this.mNetworkCalendarCount--; + } + this.mCalendarCount--; + + this.clearRefreshTimer(calendar); + }, + + setupRefreshTimer(aCalendar) { + // Add the refresh timer for this calendar + let refreshInterval = aCalendar.getProperty("refreshInterval"); + if (refreshInterval === null) { + // Default to 30 minutes, in case the value is missing + refreshInterval = 30; + } + + this.clearRefreshTimer(aCalendar); + + if (refreshInterval > 0) { + this.mRefreshTimer[aCalendar.id] = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + this.mRefreshTimer[aCalendar.id].initWithCallback( + new timerCallback(aCalendar), + refreshInterval * 60000, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + } + }, + + clearRefreshTimer(aCalendar) { + if (aCalendar.id in this.mRefreshTimer && this.mRefreshTimer[aCalendar.id]) { + this.mRefreshTimer[aCalendar.id].cancel(); + delete this.mRefreshTimer[aCalendar.id]; + } + }, + + unregisterCalendar(calendar) { + this.notifyObservers("onCalendarUnregistering", [calendar]); + this.unsetupCalendar(calendar, true); + + deletePrefBranch(calendar.id); + flushPrefs(); + }, + + removeCalendar(calendar, mode = 0) { + const cICM = Ci.calICalendarManager; + + let removeModes = new Set(calendar.getProperty("capabilities.removeModes") || ["unsubscribe"]); + if (!removeModes.has("unsubscribe") && !removeModes.has("delete")) { + // Removing is not allowed + return; + } + + if (mode & cICM.REMOVE_NO_UNREGISTER && this.mCache && calendar.id in this.mCache) { + throw new Components.Exception("Can't remove a registered calendar"); + } else if (!(mode & cICM.REMOVE_NO_UNREGISTER)) { + this.unregisterCalendar(calendar); + } + + // This observer notification needs to be fired for both unsubscribe + // and delete, we don't differ this at the moment. + this.notifyObservers("onCalendarDeleting", [calendar]); + + // For deleting, we also call the deleteCalendar method from the provider. + if (removeModes.has("delete") && (mode & cICM.REMOVE_NO_DELETE) == 0) { + let wrappedCalendar = calendar.QueryInterface(Ci.calICalendarProvider); + wrappedCalendar.deleteCalendar(calendar, null); + } + }, + + getCalendarById(aId) { + if (aId in this.mCache) { + return this.mCache[aId]; + } + return null; + }, + + getCalendars() { + this.assureCache(); + let calendars = []; + for (let id in this.mCache) { + let calendar = this.mCache[id]; + calendars.push(calendar); + } + return calendars; + }, + + /** + * Load calendars from the pref branch, if they haven't already been loaded. The calendar + * instances will end up in mCache and are refreshed when complete. + */ + assureCache() { + if (this.mCache) { + return; + } + + this.mCache = {}; + this.mCalObservers = {}; + + let allCals = {}; + for (let key of Services.prefs.getChildList(REGISTRY_BRANCH)) { + // merge down all keys + allCals[key.substring(0, key.indexOf(".", REGISTRY_BRANCH.length))] = true; + } + + for (let calBranch in allCals) { + let id = calBranch.substring(REGISTRY_BRANCH.length); + let ctype = Services.prefs.getStringPref(calBranch + ".type", null); + let curi = Services.prefs.getStringPref(calBranch + ".uri", null); + + try { + if (!ctype || !curi) { + // sanity check + deletePrefBranch(id); + continue; + } + + let uri = Services.io.newURI(curi); + let calendar = this.initializeCalendar(id, ctype, uri); + this.setupCalendar(calendar); + } catch (exc) { + cal.ERROR(`Can't create calendar for ${id} (${ctype}, ${curi}): ${exc}`); + } + } + + let shouldResyncGoogleCalDav = false; + if (!Services.prefs.prefHasUserValue("calendar.caldav.googleResync")) { + // Some users' calendars got into a bad state due to Google rate-limit + // problems so this code triggers a full resync. + shouldResyncGoogleCalDav = true; + } + + // do refreshing in a second step, when *all* calendars are already available + // via getCalendars(): + for (let calendar of Object.values(this.mCache)) { + let delay = 0; + + // The special-casing of ICS here is a very ugly hack. We can delay most + // cached calendars without an issue, but the ICS implementation has two + // properties which make that dangerous in its case: + // + // 1) ICS files can only be written whole cloth. Since it's a plain file, + // we need to know the entire contents of what we want to write. + // + // 2) It is backed by a memory calendar which it regards as its source of + // truth, and the backing calendar is only populated on a refresh. + // + // The combination of these two means that any update to the ICS calendar + // before the memory calendar is populated will erase everything in the + // calendar (except potentially the added item if that's what we're + // doing). A 15 second window for data loss-inducing updates isn't huge, + // but it's more than we should bet on. + // + // Why not fix this a different way? Trying to populate the memory + // calendar outside of a refresh causes the caching calendar to get + // confused about event ownership and identity, leading to bogus observer + // notifications and potential duplication of events in some parts of the + // interface. Having the ICS calendar refresh itself internally can cause + // disabled calendars to behave improperly, since calendars don't actually + // enforce their own disablement and may not know if they're disabled + // until after we try to refresh. Having the ICS calendar ensure it has + // refreshed itself before trying to make updates would require a fair bit + // of refactoring in its processing queue and, while it should probably + // happen, fingers crossed we can rework the provider architecture to make + // many of these problems less of an issue first. + const canDelay = calendar.getProperty("cache.enabled") && calendar.type != "ics"; + + if (canDelay) { + // If the calendar is cached, we don't need to refresh it RIGHT NOW, so let's wait a + // while and let other things happen first. + delay = 15000; + + if ( + shouldResyncGoogleCalDav && + calendar.type == "caldav" && + calendar.uri.prePath == "https://apidata.googleusercontent.com" + ) { + cal.LOG(`CalDAV: Resetting sync token of ${calendar.name} to perform a full resync`); + let calCachedCalendar = calendar.wrappedJSObject; + let calDavCalendar = calCachedCalendar.mUncachedCalendar.wrappedJSObject; + calDavCalendar.mWebdavSyncToken = null; + calDavCalendar.saveCalendarProperties(); + } + } + setTimeout(() => maybeRefreshCalendar(calendar), delay); + } + + if (shouldResyncGoogleCalDav) { + // Record the fact that we've scheduled a resync, so that we only do it once. + // Store the date instead of a boolean because we might want to use this again some day. + Services.prefs.setIntPref("calendar.caldav.googleResync", Date.now() / 1000); + } + }, + + getCalendarPref_(calendar, name) { + cal.ASSERT(calendar, "Invalid Calendar!"); + cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!"); + cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!"); + + let branch = getPrefBranchFor(calendar.id) + name; + let value = Preferences.get(branch, null); + + if (typeof value == "string" && value.startsWith("bignum:")) { + let converted = Number(value.substr(7)); + if (!isNaN(converted)) { + value = converted; + } + } + return value; + }, + + setCalendarPref_(calendar, name, value) { + cal.ASSERT(calendar, "Invalid Calendar!"); + cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!"); + cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!"); + + let branch = getPrefBranchFor(calendar.id) + name; + + if ( + typeof value == "number" && + (value > MAX_INT || value < MIN_INT || !Number.isInteger(value)) + ) { + // This is something the preferences service can't store directly. + // Convert to string and tag it so we know how to handle it. + value = "bignum:" + value; + } + + // Delete before to allow pref-type changes, then set the pref. + Services.prefs.clearUserPref(branch); + if (value !== null && value !== undefined) { + Preferences.set(branch, value); + } + }, + + deleteCalendarPref_(calendar, name) { + cal.ASSERT(calendar, "Invalid Calendar!"); + cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!"); + cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!"); + Services.prefs.clearUserPref(getPrefBranchFor(calendar.id) + name); + }, + + mObservers: null, + addObserver(aObserver) { + this.mObservers.add(aObserver); + }, + removeObserver(aObserver) { + this.mObservers.delete(aObserver); + }, + notifyObservers(functionName, args) { + this.mObservers.notify(functionName, args); + }, + + mCalendarObservers: null, + addCalendarObserver(aObserver) { + return this.mCalendarObservers.add(aObserver); + }, + removeCalendarObserver(aObserver) { + return this.mCalendarObservers.delete(aObserver); + }, + notifyCalendarObservers(functionName, args) { + this.mCalendarObservers.notify(functionName, args); + }, +}; + +function equalMessage(msg1, msg2) { + if ( + msg1.GetString(0) == msg2.GetString(0) && + msg1.GetString(1) == msg2.GetString(1) && + msg1.GetString(2) == msg2.GetString(2) + ) { + return true; + } + return false; +} + +function calMgrCalendarObserver(calendar, calMgr) { + this.calendar = calendar; + // We compare this to determine if the state actually changed. + this.storedReadOnly = calendar.readOnly; + this.announcedMessages = []; + this.calMgr = calMgr; +} + +calMgrCalendarObserver.prototype = { + calendar: null, + storedReadOnly: null, + calMgr: null, + + QueryInterface: ChromeUtils.generateQI(["nsIWindowMediatorListener", "calIObserver"]), + + // calIObserver: + onStartBatch() { + return this.calMgr.notifyCalendarObservers("onStartBatch", arguments); + }, + onEndBatch() { + return this.calMgr.notifyCalendarObservers("onEndBatch", arguments); + }, + onLoad(calendar) { + return this.calMgr.notifyCalendarObservers("onLoad", arguments); + }, + onAddItem(aItem) { + return this.calMgr.notifyCalendarObservers("onAddItem", arguments); + }, + onModifyItem(aNewItem, aOldItem) { + return this.calMgr.notifyCalendarObservers("onModifyItem", arguments); + }, + onDeleteItem(aDeletedItem) { + return this.calMgr.notifyCalendarObservers("onDeleteItem", arguments); + }, + onError(aCalendar, aErrNo, aMessage) { + this.calMgr.notifyCalendarObservers("onError", arguments); + this.announceError(aCalendar, aErrNo, aMessage); + }, + + onPropertyChanged(aCalendar, aName, aValue, aOldValue) { + this.calMgr.notifyCalendarObservers("onPropertyChanged", arguments); + switch (aName) { + case "requiresNetwork": + this.calMgr.mNetworkCalendarCount += aValue ? 1 : -1; + break; + case "readOnly": + this.calMgr.mReadonlyCalendarCount += aValue ? 1 : -1; + break; + case "refreshInterval": + this.calMgr.setupRefreshTimer(aCalendar); + break; + case "cache.enabled": + this.changeCalendarCache(...arguments); + break; + case "disabled": + if (!aValue && aCalendar.canRefresh) { + aCalendar.refresh(); + } + break; + } + }, + + changeCalendarCache(aCalendar, aName, aValue, aOldValue) { + const cICM = Ci.calICalendarManager; + aOldValue = aOldValue || false; + aValue = aValue || false; + + // hack for bug 1182264 to deal with calendars, which have set cache.enabled, but in fact do + // not support caching (like storage calendars) - this also prevents enabling cache again + if (aCalendar.getProperty("cache.supported") === false) { + if (aCalendar.getProperty("cache.enabled") === true) { + aCalendar.deleteProperty("cache.enabled"); + } + return; + } + + if (aOldValue != aValue) { + // Try to find the current sort order + let sortOrderPref = Services.prefs.getStringPref("calendar.list.sortOrder", "").split(" "); + let initialSortOrderPos = null; + for (let i = 0; i < sortOrderPref.length; ++i) { + if (sortOrderPref[i] == aCalendar.id) { + initialSortOrderPos = i; + } + } + // Enabling or disabling cache on a calendar re-creates + // it so the registerCalendar call can wrap/unwrap the + // calCachedCalendar facade saving the user the need to + // restart Thunderbird and making sure a new Id is used. + this.calMgr.removeCalendar(aCalendar, cICM.REMOVE_NO_DELETE); + let newCal = this.calMgr.createCalendar(aCalendar.type, aCalendar.uri); + newCal.name = aCalendar.name; + + // TODO: if properties get added this list will need to be adjusted, + // ideally we should add a "getProperties" method to calICalendar.idl + // to retrieve all non-transient properties for a calendar. + let propsToCopy = [ + "color", + "disabled", + "auto-enabled", + "cache.enabled", + "refreshInterval", + "suppressAlarms", + "calendar-main-in-composite", + "calendar-main-default", + "readOnly", + "imip.identity.key", + "username", + ]; + for (let prop of propsToCopy) { + newCal.setProperty(prop, aCalendar.getProperty(prop)); + } + + if (initialSortOrderPos != null) { + newCal.setProperty("initialSortOrderPos", initialSortOrderPos); + } + this.calMgr.registerCalendar(newCal); + } else if (aCalendar.wrappedJSObject instanceof calCachedCalendar) { + // any attempt to switch this flag will reset the cached calendar; + // could be useful for users in case the cache may be corrupted. + aCalendar.wrappedJSObject.setupCachedCalendar(); + } + }, + + onPropertyDeleting(aCalendar, aName) { + this.onPropertyChanged(aCalendar, aName, false, true); + }, + + // Error announcer specific functions + announceError(aCalendar, aErrNo, aMessage) { + let paramBlock = Cc["@mozilla.org/embedcomp/dialogparam;1"].createInstance( + Ci.nsIDialogParamBlock + ); + let props = Services.strings.createBundle("chrome://calendar/locale/calendar.properties"); + let errMsg; + paramBlock.SetNumberStrings(3); + if (!this.storedReadOnly && this.calendar.readOnly) { + // Major errors change the calendar to readOnly + errMsg = props.formatStringFromName("readOnlyMode", [this.calendar.name]); + } else if (!this.storedReadOnly && !this.calendar.readOnly) { + // Minor errors don't, but still tell the user something went wrong + errMsg = props.formatStringFromName("minorError", [this.calendar.name]); + } else { + // The calendar was already in readOnly mode, but still tell the user + errMsg = props.formatStringFromName("stillReadOnlyError", [this.calendar.name]); + } + + // When possible, change the error number into its name, to + // make it slightly more readable. + let errCode = "0x" + aErrNo.toString(16); + const calIErrors = Ci.calIErrors; + // Check if it is worth enumerating all the error codes. + if (aErrNo & calIErrors.ERROR_BASE) { + for (let err in calIErrors) { + if (calIErrors[err] == aErrNo) { + errCode = err; + } + } + } + + let message; + switch (aErrNo) { + case calIErrors.CAL_UTF8_DECODING_FAILED: + message = props.GetStringFromName("utf8DecodeError"); + break; + case calIErrors.ICS_MALFORMEDDATA: + message = props.GetStringFromName("icsMalformedError"); + break; + case calIErrors.MODIFICATION_FAILED: + errMsg = cal.l10n.getCalString("errorWriting2", [aCalendar.name]); + message = cal.l10n.getCalString("errorWritingDetails"); + if (aMessage) { + message = aMessage + "\n" + message; + } + break; + default: + message = aMessage; + } + + paramBlock.SetString(0, errMsg); + paramBlock.SetString(1, errCode); + paramBlock.SetString(2, message); + + this.storedReadOnly = this.calendar.readOnly; + let errorCode = cal.l10n.getCalString("errorCode", [errCode]); + let errorDescription = cal.l10n.getCalString("errorDescription", [message]); + let summary = errMsg + " " + errorCode + ". " + errorDescription; + + // Log warnings in error console. + // Report serious errors in both error console and in prompt window. + if (aErrNo == calIErrors.MODIFICATION_FAILED) { + console.error(summary); + this.announceParamBlock(paramBlock); + } else { + cal.WARN(summary); + } + }, + + announceParamBlock(paramBlock) { + function awaitLoad(event) { + promptWindow.addEventListener("unload", awaitUnload, { capture: false, once: true }); + } + let awaitUnload = event => { + // unloaded (user closed prompt window), + // remove paramBlock and unload listener. + try { + // remove the message that has been shown from + // the list of all announced messages. + this.announcedMessages = this.announcedMessages.filter(msg => { + return !equalMessage(msg, paramBlock); + }); + } catch (e) { + console.error(e); + } + }; + + // silently don't do anything if this message already has been + // announced without being acknowledged. + if (this.announcedMessages.some(equalMessage.bind(null, paramBlock))) { + return; + } + + // this message hasn't been announced recently, remember the details of + // the message for future reference. + this.announcedMessages.push(paramBlock); + + // Will remove paramBlock from announced messages when promptWindow is + // closed. (Closing fires unloaded event, but promptWindow is also + // unloaded [to clean it?] before loading, so wait for detected load + // event before detecting unload event that signifies user closed this + // prompt window.) + let promptUrl = "chrome://calendar/content/calendar-error-prompt.xhtml"; + let features = "chrome,dialog=yes,alwaysRaised=yes"; + let promptWindow = Services.ww.openWindow(null, promptUrl, "_blank", features, paramBlock); + promptWindow.addEventListener("load", awaitLoad, { capture: false, once: true }); + }, +}; + +function calDummyCalendar(type) { + this.initProviderBase(); + this.type = type; +} +calDummyCalendar.prototype = { + __proto__: cal.provider.BaseClass.prototype, + + getProperty(aName) { + switch (aName) { + case "force-disabled": + return true; + default: + return this.__proto__.__proto__.getProperty.apply(this, arguments); + } + }, +}; + +function getPrefBranchFor(id) { + return REGISTRY_BRANCH + id + "."; +} + +/** + * Removes a calendar from the preferences. + * + * @param {string} id - ID of the calendar to remove. + */ +function deletePrefBranch(id) { + for (let prefName of Services.prefs.getChildList(getPrefBranchFor(id))) { + Services.prefs.clearUserPref(prefName); + } +} + +/** + * Helper to refresh a calendar, if it can be refreshed and isn't disabled. + * + * @param {calICalendar} calendar - The calendar to refresh. + */ +function maybeRefreshCalendar(calendar) { + if (!calendar.getProperty("disabled") && calendar.canRefresh) { + let refreshInterval = calendar.getProperty("refreshInterval"); + if (refreshInterval != "0") { + calendar.refresh(); + } + } +} + +/** + * Wrap a calendar using {@link calCachedCalendar}, if the cache is supported and enabled. + * Otherwise just return the passed in calendar. + * + * @param {calICalendar} calendar - The calendar to potentially wrap. + * @returns {calICalendar} The potentially wrapped calendar. + */ +function maybeWrapCachedCalendar(calendar) { + if ( + calendar.getProperty("cache.supported") !== false && + (calendar.getProperty("cache.enabled") || calendar.getProperty("cache.always")) + ) { + calendar = new calCachedCalendar(calendar); + } + return calendar; +} + +/** + * Helper function to flush the preferences file. If the application crashes + * after a calendar has been created using the prefs registry, then the calendar + * won't show up. Writing the prefs helps counteract. + */ +function flushPrefs() { + Services.prefs.savePrefFile(null); +} + +/** + * Callback object for the refresh timer. Should be called as an object, i.e + * let foo = new timerCallback(calendar); + * + * @param aCalendar The calendar to refresh on notification + */ +function timerCallback(aCalendar) { + this.notify = function (aTimer) { + if (!aCalendar.getProperty("disabled") && aCalendar.canRefresh) { + aCalendar.refresh(); + } + }; +} + +var gCalendarManagerAddonListener = { + onDisabling(aAddon, aNeedsRestart) { + if (!this.queryUninstallProvider(aAddon)) { + // If the addon should not be disabled, then re-enable it. + aAddon.userDisabled = false; + } + }, + + onUninstalling(aAddon, aNeedsRestart) { + if (!this.queryUninstallProvider(aAddon)) { + // If the addon should not be uninstalled, then cancel the uninstall. + aAddon.cancelUninstall(); + } + }, + + queryUninstallProvider(aAddon) { + const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xhtml"; + const features = "chrome,titlebar,resizable,modal"; + let affectedCalendars = cal.manager + .getCalendars() + .filter(calendar => calendar.providerID == aAddon.id); + if (!affectedCalendars.length) { + // If no calendars are affected, then everything is fine. + return true; + } + + let args = { shouldUninstall: false, extension: aAddon }; + + // Now find a window. The best choice would be the most recent + // addons window, otherwise the most recent calendar window, or we + // create a new toplevel window. + let win = + Services.wm.getMostRecentWindow("Extension:Manager") || cal.window.getCalendarWindow(); + if (win) { + win.openDialog(uri, "CalendarProviderUninstallDialog", features, args); + } else { + // Use the window watcher to open a parentless window. + Services.ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args); + } + + // Now that we are done, check if the dialog was accepted or canceled. + return args.shouldUninstall; + }, +}; + +function appendToRealm(authHeader, appendStr) { + let isEscaped = false; + let idx = authHeader.search(/realm="(.*?)(\\*)"/); + if (idx > -1) { + let remain = authHeader.substr(idx + 7); + idx += 7; + while (remain.length && !isEscaped) { + let match = remain.match(/(.*?)(\\*)"/); + idx += match[0].length; + + isEscaped = match[2].length % 2 == 0; + if (!isEscaped) { + remain = remain.substr(match[0].length); + } + } + return authHeader.substr(0, idx - 1) + " " + appendStr + authHeader.substr(idx - 1); + } + return authHeader; +} |