summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/src/CalCalendarManager.jsm
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/calendar/base/src/CalCalendarManager.jsm
parentInitial commit. (diff)
downloadthunderbird-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.jsm1076
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;
+}