diff options
Diffstat (limited to 'calendar/base/src/calCalendarManager.js')
-rw-r--r-- | calendar/base/src/calCalendarManager.js | 1123 |
1 files changed, 1123 insertions, 0 deletions
diff --git a/calendar/base/src/calCalendarManager.js b/calendar/base/src/calCalendarManager.js new file mode 100644 index 000000000..6800da45a --- /dev/null +++ b/calendar/base/src/calCalendarManager.js @@ -0,0 +1,1123 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/AddonManager.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Preferences.jsm"); +Components.utils.import("resource://calendar/modules/calUtils.jsm"); +Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); + +var REGISTRY_BRANCH = "calendar.registry."; +var DB_SCHEMA_VERSION = 10; +var MAX_INT = Math.pow(2, 31) - 1; +var MIN_INT = -MAX_INT; + +function calCalendarManager() { + this.wrappedJSObject = this; + this.mObservers = new calListenerBag(Components.interfaces.calICalendarManagerObserver); + this.mCalendarObservers = new calListenerBag(Components.interfaces.calIObserver); +} + +var calCalendarManagerClassID = Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}"); +var calCalendarManagerInterfaces = [ + Components.interfaces.calICalendarManager, + Components.interfaces.calIStartupService, + Components.interfaces.nsIObserver, +]; +calCalendarManager.prototype = { + classID: calCalendarManagerClassID, + QueryInterface: XPCOMUtils.generateQI(calCalendarManagerInterfaces), + classInfo: XPCOMUtils.generateCI({ + classID: calCalendarManagerClassID, + contractID: "@mozilla.org/calendar/manager;1", + classDescription: "Calendar Manager", + interfaces: calCalendarManagerInterfaces, + flags: Components.interfaces.nsIClassInfo.SINGLETON + }), + + get networkCalendarCount() { return this.mNetworkCalendarCount; }, + get readOnlyCalendarCount() { return this.mReadonlyCalendarCount; }, + get calendarCount() { return this.mCalendarCount; }, + + // calIStartupService: + startup: function(aCompleteListener) { + AddonManager.addAddonListener(gCalendarManagerAddonListener); + this.checkAndMigrateDB(); + this.mCache = null; + this.mCalObservers = null; + this.mRefreshTimer = {}; + this.setupOfflineObservers(); + this.mNetworkCalendarCount = 0; + this.mReadonlyCalendarCount = 0; + this.mCalendarCount = 0; + + Services.obs.addObserver(this, "http-on-modify-request", false); + + // 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 (Preferences.get("calendar.network.multirealm", false)) { + Services.obs.addObserver(this, "http-on-examine-response", false); + } + + aCompleteListener.onResult(null, Components.results.NS_OK); + }, + + shutdown: function(aCompleteListener) { + for (let id in this.mCache) { + let calendar = this.mCache[id]; + calendar.removeObserver(this.mCalObservers[calendar.id]); + } + + this.cleanupOfflineObservers(); + + Services.obs.removeObserver(this, "http-on-modify-request"); + + 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 (Preferences.get("calendar.network.multirealm", false)) { + Services.obs.removeObserver(this, "http-on-examine-response"); + } + + aCompleteListener.onResult(null, Components.results.NS_OK); + }, + + + setupOfflineObservers: function() { + Services.obs.addObserver(this, "network:offline-status-changed", false); + }, + + cleanupOfflineObservers: function() { + Services.obs.removeObserver(this, "network:offline-status-changed"); + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "timer-callback": { + // Refresh all the calendars that can be refreshed. + for (let calendar of this.getCalendars({})) { + if (!calendar.getProperty("disabled") && calendar.canRefresh) { + calendar.refresh(); + } + } + 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(Components.interfaces.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(Components.interfaces.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 != Components.results.NS_NOINTERFACE && + e.result != Components.results.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 (thats ok) + } + break; + } + case "http-on-modify-request": { + // Unfortunately, the ability to do this with a general pref has + // been removed. Calendar servers might still want to know what + // client is used for access, so add our UA String to each + // request. + let httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel); + try { + // NOTE: For some reason, this observer call doesn't have + // the "cal" namespace defined + let userAgent = httpChannel.getRequestHeader("User-Agent"); + let calUAString = Preferences.get("calendar.useragent.extra", "").trim(); + + // Don't add an empty string or an already included token. + if (calUAString && !userAgent.includes(calUAString)) { + // User-Agent is not a mergeable header. We need to + // merge the user agent ourselves. + httpChannel.setRequestHeader("User-Agent", + userAgent + " " + calUAString, + false); + } + } catch (e) { + if (e.result != Components.results.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + // We swallow this error since it means the User Agent + // header is not set. We don't want to force it to be set. + } + break; + } + } + }, + + // + // DB migration code begins here + // + + upgradeDB: function(oldVersion, db) { + if (oldVersion < 6) { + dump("**** Upgrading calCalendarManager schema to 6\n"); + + // Schema changes in v6: + // + // - Change all STRING columns to TEXT to avoid SQLite's + // "feature" where it will automatically convert strings to + // numbers (ex: 10e4 -> 10000). See bug 333688. + + // Create the new tables. + + try { + db.executeSimpleSQL("DROP TABLE cal_calendars_v6; DROP TABLE cal_calendars_prefs_v6;"); + } catch (e) { + // We should get exceptions for trying to drop tables + // that don't (shouldn't) exist. + } + + db.executeSimpleSQL("CREATE TABLE cal_calendars_v6 " + + "(id INTEGER PRIMARY KEY," + + " type TEXT," + + " uri TEXT);"); + + db.executeSimpleSQL("CREATE TABLE cal_calendars_prefs_v6 " + + "(id INTEGER PRIMARY KEY," + + " calendar INTEGER," + + " name TEXT," + + " value TEXT);"); + + // Copy in the data. + let calendarCols = ["id", "type", "uri"]; + let calendarPrefsCols = ["id", "calendar", "name", "value"]; + + db.executeSimpleSQL("INSERT INTO cal_calendars_v6(" + calendarCols.join(",") + ") " + + " SELECT " + calendarCols.join(",") + + " FROM cal_calendars"); + + db.executeSimpleSQL("INSERT INTO cal_calendars_prefs_v6(" + calendarPrefsCols.join(",") + ") " + + " SELECT " + calendarPrefsCols.join(",") + + " FROM cal_calendars_prefs"); + + // Delete each old table and rename the new ones to use the + // old tables' names. + let tableNames = ["cal_calendars", "cal_calendars_prefs"]; + + for (let i in tableNames) { + db.executeSimpleSQL("DROP TABLE " + tableNames[i] + ";" + + "ALTER TABLE " + tableNames[i] + "_v6 " + + " RENAME TO " + tableNames[i] + ";"); + } + + oldVersion = 8; + } + + if (oldVersion < DB_SCHEMA_VERSION) { + dump("**** Upgrading calCalendarManager schema to 9/10\n"); + + if (db.tableExists("cal_calmgr_schema_version")) { + // Set only once the last time to v10, so the version check works in calendar 0.8. + // In calendar 0.9 and following, the provider itself will check its version + // on initialization and notify the calendar whether it's usable or not. + db.executeSimpleSQL("UPDATE cal_calmgr_schema_version SET version = " + DB_SCHEMA_VERSION + ";"); + } else { + // Schema changes in v9: + // + // - Decouple schema version from storage calendar + // Create the new tables. + db.executeSimpleSQL("CREATE TABLE cal_calmgr_schema_version (version INTEGER);"); + db.executeSimpleSQL("INSERT INTO cal_calmgr_schema_version VALUES(" + DB_SCHEMA_VERSION + ")"); + } + } + }, + + migrateDB: function(db) { + let selectCalendars = db.createStatement("SELECT * FROM cal_calendars"); + let selectPrefs = db.createStatement("SELECT name, value FROM cal_calendars_prefs WHERE calendar = :calendar"); + try { + let sortOrder = {}; + + while (selectCalendars.executeStep()) { + let id = cal.getUUID(); // use fresh uuids + Preferences.set(getPrefBranchFor(id) + "type", selectCalendars.row.type); + Preferences.set(getPrefBranchFor(id) + "uri", selectCalendars.row.uri); + // the former id served as sort position: + sortOrder[selectCalendars.row.id] = id; + // move over prefs: + selectPrefs.params.calendar = selectCalendars.row.id; + while (selectPrefs.executeStep()) { + let name = selectPrefs.row.name.toLowerCase(); // may come lower case, so force it to be + let value = selectPrefs.row.value; + switch (name) { + case "readonly": + Preferences.set(getPrefBranchFor(id) + "readOnly", value == "true"); + break; + case "relaxedmode": + Preferences.set(getPrefBranchFor(id) + "relaxedMode", value == "true"); + break; + case "suppressalarms": + Preferences.set(getPrefBranchFor(id) + "suppressAlarms", value == "true"); + break; + case "disabled": + case "cache.supported": + case "auto-enabled": + case "cache.enabled": + case "lightning-main-in-composite": + case "calendar-main-in-composite": + case "lightning-main-default": + case "calendar-main-default": + Preferences.set(getPrefBranchFor(id) + name, value == "true"); + break; + case "backup-time": + case "uniquenum": + // These preference names were migrated due to bug 979262. + Preferences.set(getPrefBranchFor(id) + name + "2", "bignum:" + value); + break; + default: // keep as string + Preferences.set(getPrefBranchFor(id) + name, value); + break; + } + } + selectPrefs.reset(); + } + + let sortOrderAr = []; + for (let id in sortOrder) { + sortOrderAr.push(sortOrder[id]); + } + Preferences.set("calendar.list.sortOrder", sortOrderAr.join(" ")); + flushPrefs(); + } finally { + selectPrefs.reset(); + selectCalendars.reset(); + } + }, + + checkAndMigrateDB: function() { + let storageSdb = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile); + storageSdb.append("storage.sdb"); + let db = Services.storage.openDatabase(storageSdb); + + db.beginTransactionAs(Components.interfaces.mozIStorageConnection.TRANSACTION_EXCLUSIVE); + try { + if (db.tableExists("cal_calendars_prefs")) { + // Check if we need to upgrade: + let version = this.getSchemaVersion(db); + if (version < DB_SCHEMA_VERSION) { + this.upgradeDB(version, db); + } + + this.migrateDB(db); + + db.executeSimpleSQL("DROP TABLE cal_calendars; " + + "DROP TABLE cal_calendars_prefs; " + + "DROP TABLE cal_calmgr_schema_version;"); + } + + if (db.tableExists("cal_calendars")) { + db.rollbackTransaction(); + } else { + // create dummy cal_calendars, so previous versions (pre 1.0pre) run into the schema check: + db.createTable("cal_calendars", "id INTEGER"); + // let schema checks always fail, we cannot take the shared cal_calendar_schema_version: + db.createTable("cal_calmgr_schema_version", "version INTEGER"); + db.executeSimpleSQL("INSERT INTO cal_calmgr_schema_version VALUES(" + (DB_SCHEMA_VERSION + 1) + ")"); + db.commitTransaction(); + } + } catch (exc) { + db.rollbackTransaction(); + throw exc; + } finally { + db.close(); + } + }, + + /** + * @return db schema version + * @exception various, depending on error + */ + getSchemaVersion: function(db) { + let stmt; + let version = null; + + let table; + if (db.tableExists("cal_calmgr_schema_version")) { + table = "cal_calmgr_schema_version"; + } else { + // Fall back to the old schema table + table = "cal_calendar_schema_version"; + } + + try { + stmt = db.createStatement("SELECT version FROM " + table + " LIMIT 1"); + if (stmt.executeStep()) { + version = stmt.row.version; + } + stmt.reset(); + + if (version !== null) { + // This is the only place to leave this function gracefully. + return version; + } + } catch (e) { + if (stmt) { + stmt.reset(); + } + cal.ERROR("++++++++++++ calMgrGetSchemaVersion() error: " + db.lastErrorString); + Components.utils.reportError("Error getting calendar schema version! DB Error: " + db.lastErrorString); + throw e; + } + + throw table + " SELECT returned no results"; + }, + + // + // / DB migration code ends here + // + + alertAndQuit: function() { + // We want to include the extension name in the error message rather + // than blaming Thunderbird. + let hostAppName = calGetString("brand", "brandShortName", null, "branding"); + let calAppName = calGetString("lightning", "brandShortName", null, "lightning"); + let errorBoxTitle = calGetString("calendar", "tooNewSchemaErrorBoxTitle", [calAppName]); + let errorBoxText = calGetString("calendar", "tooNewSchemaErrorBoxTextLightning", [calAppName, hostAppName]); + let errorBoxButtonLabel = calGetString("calendar", "tooNewSchemaButtonRestart", [hostAppName]); + + let promptSvc = Services.prompt; + + let errorBoxButtonFlags = promptSvc.BUTTON_POS_0 * + promptSvc.BUTTON_TITLE_IS_STRING + + promptSvc.BUTTON_POS_0_DEFAULT; + + promptSvc.confirmEx(null, + errorBoxTitle, + errorBoxText, + errorBoxButtonFlags, + errorBoxButtonLabel, + null, // No second button text + null, // No third button text + null, // No checkbox + { value: false }); // Unnecessary checkbox state + + // Disable Lightning + AddonManager.getAddonByID("{e2fda1a4-762b-4020-b5ad-a41df1933103}", (aAddon) => { + aAddon.userDisabled = true; + Services.startup.quit(Components.interfaces.nsIAppStartup.eRestart | + Components.interfaces.nsIAppStartup.eForceQuit); + }); + }, + + /** + * calICalendarManager interface + */ + createCalendar: function(type, uri) { + try { + if (!Components.classes["@mozilla.org/calendar/calendar;1?type=" + type]) { + // Don't notify the user with an extra dialog if the provider + // interface is missing. + return null; + } + let calendar = Components.classes["@mozilla.org/calendar/calendar;1?type=" + type] + .createInstance(Components.interfaces.calICalendar); + calendar.uri = uri; + return calendar; + } catch (ex) { + let rc = ex; + let uiMessage = ex; + if (ex instanceof Components.interfaces.nsIException) { + rc = ex.result; + uiMessage = ex.message; + } + switch (rc) { + case Components.interfaces.calIErrors.STORAGE_UNKNOWN_SCHEMA_ERROR: + // For now we alert and quit on schema errors like we've done before: + this.alertAndQuit(); + return null; + case Components.interfaces.calIErrors.STORAGE_UNKNOWN_TIMEZONES_ERROR: + uiMessage = calGetString("calendar", "unknownTimezonesError", [uri.spec]); + break; + default: + uiMessage = calGetString("calendar", "unableToCreateProvider", [uri.spec]); + break; + } + // 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 = Components.classes["@mozilla.org/embedcomp/dialogparam;1"] + .createInstance(Components.interfaces.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.xul", + "_blank", + "chrome,dialog=yes,alwaysRaised=yes", + paramBlock); + return null; + } + }, + + registerCalendar: function(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(); + } + + Preferences.set(getPrefBranchFor(calendar.id) + "type", calendar.type); + Preferences.set(getPrefBranchFor(calendar.id) + "uri", calendar.uri.spec); + + if ((calendar.getProperty("cache.supported") !== false) && + (calendar.getProperty("cache.enabled") || + calendar.getProperty("cache.always"))) { + calendar = new calCachedCalendar(calendar); + } + + this.setupCalendar(calendar); + flushPrefs(); + + if (!calendar.getProperty("disabled") && calendar.canRefresh) { + calendar.refresh(); + } + + this.notifyObservers("onCalendarRegistered", [calendar]); + }, + + setupCalendar: function(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); + }, + + setupRefreshTimer: function(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] = + Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + + this.mRefreshTimer[aCalendar.id] + .initWithCallback(new timerCallback(aCalendar), + refreshInterval * 60000, + Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); + } + }, + + clearRefreshTimer: function(aCalendar) { + if (aCalendar.id in this.mRefreshTimer && + this.mRefreshTimer[aCalendar.id]) { + this.mRefreshTimer[aCalendar.id].cancel(); + delete this.mRefreshTimer[aCalendar.id]; + } + }, + + unregisterCalendar: function(calendar) { + this.notifyObservers("onCalendarUnregistering", [calendar]); + + // calendar may be a calICalendar wrapper: + if (calendar.wrappedJSObject instanceof calCachedCalendar) { + calendar.wrappedJSObject.onCalendarUnregistering(); + } + + calendar.removeObserver(this.mCalObservers[calendar.id]); + Services.prefs.deleteBranch(getPrefBranchFor(calendar.id)); + flushPrefs(); + + if (this.mCache) { + delete this.mCache[calendar.id]; + } + + if (calendar.readOnly) { + this.mReadonlyCalendarCount--; + } + + if (calendar.getProperty("requiresNetwork") !== false) { + this.mNetworkCalendarCount--; + } + this.mCalendarCount--; + + this.clearRefreshTimer(calendar); + }, + + // Delete this method for Lightning 4.7 at latest + deleteCalendar: function(calendar) { + if (!this.deleteCalendar.warningIssued) { + cal.WARN("Use of calICalendarManager::deleteCalendar is deprecated" + + " and will be removed with the next release. Use" + + " ::removeCalendar instead.\n" + cal.STACK(10)); + this.deleteCalendar.warningIssued = true; + } + + const cICM = Components.interfaces.calICalendarManager; + this.removeCalendar(calendar, cICM.REMOVE_NO_UNREGISTER); + }, + + removeCalendar: function(calendar, mode=0) { + const cICM = Components.interfaces.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 = cal.wrapInstance(calendar, Components.interfaces.calICalendarProvider); + if (!wrappedCalendar) { + throw new Components.Exception("Calendar is missing a provider implementation for delete"); + } + + wrappedCalendar.deleteCalendar(calendar, null); + } + }, + + getCalendarById: function(aId) { + if (aId in this.mCache) { + return this.mCache[aId]; + } else { + return null; + } + }, + + getCalendars: function(count) { + this.assureCache(); + let calendars = []; + for (let id in this.mCache) { + let calendar = this.mCache[id]; + calendars.push(calendar); + } + count.value = calendars.length; + return calendars; + }, + + assureCache: function() { + if (!this.mCache) { + 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 = Preferences.get(calBranch + ".type", null); + let curi = Preferences.get(calBranch + ".uri", null); + + try { + if (!ctype || !curi) { // sanity check + Services.prefs.deleteBranch(calBranch + "."); + continue; + } + + let uri = cal.makeURL(curi); + let calendar = this.createCalendar(ctype, uri); + if (calendar) { + calendar.id = id; + if (calendar.getProperty("auto-enabled")) { + calendar.deleteProperty("disabled"); + calendar.deleteProperty("auto-enabled"); + } + + if ((calendar.getProperty("cache.supported") !== false) && + (calendar.getProperty("cache.enabled") || + calendar.getProperty("cache.always"))) { + calendar = new calCachedCalendar(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); + } + + this.setupCalendar(calendar); + } catch (exc) { + cal.ERROR("Can't create calendar for " + id + " (" + ctype + ", " + curi + "): " + exc); + } + } + + // do refreshing in a second step, when *all* calendars are already available + // via getCalendars(): + for (let id in this.mCache) { + let calendar = this.mCache[id]; + if (!calendar.getProperty("disabled") && calendar.canRefresh) { + calendar.refresh(); + } + } + } + }, + + getCalendarPref_: function(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_: function(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.deleteBranch(branch); + if (value !== null && value !== undefined) { + Preferences.set(branch, value); + } + }, + + deleteCalendarPref_: function(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.deleteBranch(getPrefBranchFor(calendar.id) + name); + }, + + mObservers: null, + addObserver: function(aObserver) { return this.mObservers.add(aObserver); }, + removeObserver: function(aObserver) { return this.mObservers.remove(aObserver); }, + notifyObservers: function(functionName, args) { return this.mObservers.notify(functionName, args); }, + + mCalendarObservers: null, + addCalendarObserver: function(aObserver) { return this.mCalendarObservers.add(aObserver); }, + removeCalendarObserver: function(aObserver) { return this.mCalendarObservers.remove(aObserver); }, + notifyCalendarObservers: function(functionName, args) { return 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: XPCOMUtils.generateQI([ + Components.interfaces.nsIWindowMediatorListener, + Components.interfaces.calIObserver + ]), + + // calIObserver: + onStartBatch: function() { return this.calMgr.notifyCalendarObservers("onStartBatch", arguments); }, + onEndBatch: function() { return this.calMgr.notifyCalendarObservers("onEndBatch", arguments); }, + onLoad: function(calendar) { return this.calMgr.notifyCalendarObservers("onLoad", arguments); }, + onAddItem: function(aItem) { return this.calMgr.notifyCalendarObservers("onAddItem", arguments); }, + onModifyItem: function(aNewItem, aOldItem) { return this.calMgr.notifyCalendarObservers("onModifyItem", arguments); }, + onDeleteItem: function(aDeletedItem) { return this.calMgr.notifyCalendarObservers("onDeleteItem", arguments); }, + onError: function(aCalendar, aErrNo, aMessage) { + this.calMgr.notifyCalendarObservers("onError", arguments); + this.announceError(aCalendar, aErrNo, aMessage); + }, + + onPropertyChanged: function(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: function(aCalendar, aName, aValue, aOldValue) { + const cICM = Components.interfaces.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 = Preferences.get("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" + ]; + 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: function(aCalendar, aName) { + this.onPropertyChanged(aCalendar, aName, false, true); + }, + + // Error announcer specific functions + announceError: function(aCalendar, aErrNo, aMessage) { + let paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"] + .createInstance(Components.interfaces.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], 1); + } 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], 1); + } else { + // The calendar was already in readOnly mode, but still tell the user + errMsg = props.formatStringFromName("stillReadOnlyError", [this.calendar.name], 1); + } + + // When possible, change the error number into its name, to + // make it slightly more readable. + let errCode = "0x" + aErrNo.toString(16); + const calIErrors = Components.interfaces.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 = calGetString("calendar", "errorWriting", [aCalendar.name]); + // falls through + default: + message = aMessage; + } + + + paramBlock.SetString(0, errMsg); + paramBlock.SetString(1, errCode); + paramBlock.SetString(2, message); + + this.storedReadOnly = this.calendar.readOnly; + let errorCode = calGetString("calendar", "errorCode", [errCode]); + let errorDescription = calGetString("calendar", "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) { + Components.utils.reportError(summary); + this.announceParamBlock(paramBlock); + } else { + cal.WARN(summary); + } + }, + + announceParamBlock: function(paramBlock) { + function awaitLoad(event) { + promptWindow.removeEventListener("load", awaitLoad, false); + promptWindow.addEventListener("unload", awaitUnload, false); + } + let awaitUnload = (event) => { + promptWindow.removeEventListener("unload", awaitUnload, false); + // 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) { + Components.utils.reportError(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.xul"; + let features = "chrome,dialog=yes,alwaysRaised=yes"; + let promptWindow = Services.ww.openWindow(null, promptUrl, "_blank", features, paramBlock); + promptWindow.addEventListener("load", awaitLoad, false); + } +}; + +function calDummyCalendar(type) { + this.initProviderBase(); + this.type = type; +} +calDummyCalendar.prototype = { + __proto__: cal.ProviderBase.prototype, + + getProperty: function(aName) { + switch (aName) { + case "force-disabled": + return true; + default: + return this.__proto__.__proto__.getProperty.apply(this, arguments); + } + } +}; + +function getPrefBranchFor(id) { + return REGISTRY_BRANCH + id + "."; +} + +/** + * 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: function(aAddon, aNeedsRestart) { + if (!this.queryUninstallProvider(aAddon)) { + // If the addon should not be disabled, then re-enable it. + aAddon.userDisabled = false; + } + }, + + onUninstalling: function(aAddon, aNeedsRestart) { + if (!this.queryUninstallProvider(aAddon)) { + // If the addon should not be uninstalled, then cancel the uninstall. + aAddon.cancelUninstall(); + } + }, + + queryUninstallProvider: function(aAddon) { + const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xul"; + const features = "chrome,titlebar,resizable,modal"; + let calMgr = cal.getCalendarManager(); + let affectedCalendars = + calMgr.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.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); + } else { + return authHeader; + } +} |