Created
June 8, 2011 13:51
-
-
Save creativepsyco/1014460 to your computer and use it in GitHub Desktop.
calcachedCalendar [2nd Day]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ***** BEGIN LICENSE BLOCK ***** | |
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 | |
* | |
* The contents of this file are subject to the Mozilla Public License Version | |
* 1.1 (the "License"); you may not use this file except in compliance with | |
* the License. You may obtain a copy of the License at | |
* http://www.mozilla.org/MPL/ | |
* | |
* Software distributed under the License is distributed on an "AS IS" basis, | |
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | |
* for the specific language governing rights and limitations under the | |
* License. | |
* | |
* The Original Code is Oracle Corporation code. | |
* | |
* The Initial Developer of the Original Code is Oracle Corporation | |
* Portions created by the Initial Developer are Copyright (C) 2005 | |
* the Initial Developer. All Rights Reserved. | |
* | |
* Contributor(s): | |
* Stuart Parmenter <[email protected]> | |
* Matthew Willis <[email protected]> | |
* Michiel van Leeuwen <[email protected]> | |
* Martin Schroeder <[email protected]> | |
* Philipp Kewisch <[email protected]> | |
* Daniel Boelzle <[email protected]> | |
* | |
* Alternatively, the contents of this file may be used under the terms of | |
* either the GNU General Public License Version 2 or later (the "GPL"), or | |
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), | |
* in which case the provisions of the GPL or the LGPL are applicable instead | |
* of those above. If you wish to allow use of your version of this file only | |
* under the terms of either the GPL or the LGPL, and not to allow others to | |
* use your version of this file under the terms of the MPL, indicate your | |
* decision by deleting the provisions above and replace them with the notice | |
* and other provisions required by the GPL or the LGPL. If you do not delete | |
* the provisions above, a recipient may use your version of this file under | |
* the terms of any one of the MPL, the GPL or the LGPL. | |
* | |
* ***** END LICENSE BLOCK ***** */ | |
Components.utils.import("resource://calendar/modules/calUtils.jsm"); | |
Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); | |
const REGISTRY_BRANCH = "calendar.registry."; | |
const DB_SCHEMA_VERSION = 11; | |
function getPrefBranchFor(id) { | |
return (REGISTRY_BRANCH + id + "."); | |
} | |
function createStatement(dbconn, sql) { | |
let stmt = dbconn.createStatement(sql); | |
let wrapper = Components.classes["@mozilla.org/storage/statement-wrapper;1"] | |
.createInstance(Components.interfaces.mozIStorageStatementWrapper); | |
wrapper.initialize(stmt); | |
return wrapper; | |
} | |
/** | |
* 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() { | |
Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefService) | |
.savePrefFile(null); | |
} | |
function calCalendarManager() { | |
this.wrappedJSObject = this; | |
this.mObservers = new calListenerBag(Components.interfaces.calICalendarManagerObserver); | |
this.setUpStartupObservers(); | |
} | |
var calCalendarManagerClassInfo = { | |
getInterfaces: function (count) { | |
var ifaces = [ | |
Components.interfaces.nsISupports, | |
Components.interfaces.calICalendarManager, | |
Components.interfaces.nsIObserver, | |
Components.interfaces.nsIClassInfo | |
]; | |
count.value = ifaces.length; | |
return ifaces; | |
}, | |
getHelperForLanguage: function (language) { | |
return null; | |
}, | |
contractID: "@mozilla.org/calendar/manager;1", | |
classDescription: "Calendar Manager", | |
classID: Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}"), | |
implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT, | |
flags: Components.interfaces.nsIClassInfo.SINGLETON | |
}; | |
calCalendarManager.prototype = { | |
QueryInterface: function (aIID) { | |
return doQueryInterface(this, | |
calCalendarManager.prototype, | |
aIID, | |
null, | |
calCalendarManagerClassInfo); | |
}, | |
get networkCalendarCount() { | |
return this.mNetworkCalendarCount; | |
}, | |
get readOnlyCalendarCount() { | |
return this.mReadonlyCalendarCount; | |
}, | |
get calendarCount() { | |
return this.mCalendarCount; | |
}, | |
setUpStartupObservers: function ccm_setUpStartupObservers() { | |
var observerSvc = Components.classes["@mozilla.org/observer-service;1"] | |
.getService(Components.interfaces.nsIObserverService); | |
observerSvc.addObserver(this, "profile-after-change", false); | |
observerSvc.addObserver(this, "profile-before-change", false); | |
observerSvc.addObserver(this, "em-action-requested", false); | |
}, | |
startup: function ccm_startup() { | |
this.checkAndMigrateDB(); | |
this.mCache = null; | |
this.mCalObservers = null; | |
this.mRefreshTimer = null; | |
this.setUpPrefObservers(); | |
this.setUpRefreshTimer(); | |
this.setupOfflineObservers(); | |
if (cal.isSunbird()) { | |
this.loginMasterPassword(); | |
} | |
this.mNetworkCalendarCount = 0; | |
this.mReadonlyCalendarCount = 0; | |
this.mCalendarCount = 0; | |
}, | |
shutdown: function ccm_shutdown() { | |
for each (var cal in this.mCache) { | |
cal.removeObserver(this.mCalObservers[cal.id]); | |
} | |
this.cleanupPrefObservers(); | |
this.cleanupOfflineObservers(); | |
var observerSvc = Components.classes["@mozilla.org/observer-service;1"] | |
.getService(Components.interfaces.nsIObserverService); | |
observerSvc.removeObserver(this, "profile-after-change"); | |
observerSvc.removeObserver(this, "profile-before-change"); | |
observerSvc.removeObserver(this, "em-action-requested"); | |
}, | |
setUpPrefObservers: function ccm_setUpPrefObservers() { | |
var prefBranch = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch2); | |
prefBranch.addObserver("calendar.autorefresh.enabled", this, false); | |
prefBranch.addObserver("calendar.autorefresh.timeout", this, false); | |
}, | |
cleanupPrefObservers: function ccm_cleanupPrefObservers() { | |
var prefBranch = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch2); | |
prefBranch.removeObserver("calendar.autorefresh.enabled", this); | |
prefBranch.removeObserver("calendar.autorefresh.timeout", this); | |
}, | |
setUpRefreshTimer: function ccm_setUpRefreshTimer() { | |
if (this.mRefreshTimer) { | |
this.mRefreshTimer.cancel(); | |
} | |
var prefBranch = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch); | |
var refreshEnabled = false; | |
try { | |
var refreshEnabled = prefBranch.getBoolPref("calendar.autorefresh.enabled"); | |
} catch (e) { | |
} | |
// Read and convert the minute-based pref to msecs | |
var refreshTimeout = 0; | |
try { | |
var refreshTimeout = prefBranch.getIntPref("calendar.autorefresh.timeout") * 60000; | |
} catch (e) { | |
} | |
if (refreshEnabled && refreshTimeout > 0) { | |
this.mRefreshTimer = Components.classes["@mozilla.org/timer;1"] | |
.createInstance(Components.interfaces.nsITimer); | |
this.mRefreshTimer.init(this, refreshTimeout, | |
Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); | |
} | |
}, | |
setupOfflineObservers: function ccm_setupOfflineObservers() { | |
var os = Components.classes["@mozilla.org/observer-service;1"] | |
.getService(Components.interfaces.nsIObserverService); | |
os.addObserver(this, "network:offline-status-changed", false); | |
}, | |
cleanupOfflineObservers: function ccm_cleanupOfflineObservers() { | |
var os = Components.classes["@mozilla.org/observer-service;1"] | |
.getService(Components.interfaces.nsIObserverService); | |
os.removeObserver(this, "network:offline-status-changed"); | |
}, | |
loginMasterPassword: function ccm_loginMasterPassword() { | |
// Try to avoid the multiple master password prompts on startup scenario | |
// by prompting for the master password upfront. | |
let token = Components.classes["@mozilla.org/security/pk11tokendb;1"] | |
.getService(Components.interfaces.nsIPK11TokenDB) | |
.getInternalKeyToken(); | |
// Only log in to the internal token if it is already initialized, | |
// otherwise we get a "Change Master Password" dialog. | |
try { | |
if (!token.needsUserInit) { | |
token.login(false); | |
} | |
} catch (ex) { | |
// If user cancels an exception is expected. | |
} | |
}, | |
observe: function ccm_observe(aSubject, aTopic, aData) { | |
switch (aTopic) { | |
case "profile-after-change": | |
this.startup(); | |
break; | |
case "profile-before-change": | |
this.shutdown(); | |
break; | |
case "timer-callback": | |
// Refresh all the calendars that can be refreshed. | |
var cals = this.getCalendars({}); | |
for each (var cal in cals) { | |
if (!cal.getProperty("disabled") && cal.canRefresh) { | |
cal.refresh(); | |
} | |
} | |
break; | |
case "nsPref:changed": | |
if (aData == "calendar.autorefresh.enabled" || | |
aData == "calendar.autorefresh.timeout") { | |
this.setUpRefreshTimer(); | |
} | |
break; | |
case "network:offline-status-changed": | |
for each (var calendar in this.mCache) { | |
if (calendar instanceof calCachedCalendar) { | |
calendar.onOfflineStatusChanged(aData == "offline"); | |
} | |
} | |
break; | |
case "em-action-requested": | |
let extension = aSubject.QueryInterface(Components.interfaces.nsIUpdateItem); | |
let extMgr = Components.classes["@mozilla.org/extensions/manager;1"] | |
.getService(Components.interfaces.nsIExtensionManager); | |
try { | |
switch (aData) { | |
case "item-disabled": | |
if (!this.queryUninstallProvider(extension)) { | |
// If the extension should not be disabled, | |
// then re-enable it. | |
extMgr.enableItem(extension.id); | |
} | |
break; | |
case "item-uninstalled": | |
if (!this.queryUninstallProvider(extension)) { | |
// If the extension should not be uninstalled, | |
// then cancel the uninstall | |
extMgr.cancelUninstallItem(extension.id); | |
} | |
break; | |
} | |
} catch (e) { | |
// It seems this observer swallows exceptions | |
cal.ERROR(e); | |
} | |
break; | |
} | |
}, | |
queryUninstallProvider: function cCM_queryUninstallProvider(aExtension) { | |
const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xul"; | |
const features = "chrome,titlebar,resizable,modal"; | |
let affectedCalendars = | |
[ cal for each (cal in this.getCalendars({})) | |
if (cal.providerID == aExtension.id) ]; | |
if (!affectedCalendars.length) { | |
// If no calendars are affected, then everything is fine. | |
return true; | |
} | |
let args = { shouldUninstall: false, extension: aExtension }; | |
// 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 wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] | |
.getService(Components.interfaces.nsIWindowMediator); | |
let win = wm.getMostRecentWindow("Extension:Manager") || | |
cal.getCalendarWindow(); | |
if (win) { | |
win.openDialog(uri, "CalendarProviderUninstallDialog", features, args); | |
} else { | |
// Use the window watcher to open a parentless window. | |
let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] | |
.getService(Components.interfaces.nsIWindowWatcher); | |
ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args); | |
} | |
// Now that we are done, check if the dialog was accepted or canceled. | |
return args.shouldUninstall; | |
}, | |
// | |
// DB migration code begins here | |
// | |
upgradeDB: function(oldVersion, db) { | |
// some common helpers | |
function addColumn(db_, tableName, colName, colType) { | |
db_.executeSimpleSQL("ALTER TABLE " + tableName + " ADD COLUMN " + colName + " " + colType); | |
} | |
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. | |
var calendarCols = ["id", "type", "uri"]; | |
var 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. | |
var tableNames = ["cal_calendars", "cal_calendars_prefs"]; | |
for (var 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 + ")"); | |
} | |
//create an offline journal column in cal_events | |
if(db.tableExists("cal_events")){ | |
addColumn(db,"cal_events","offline_journal","TEXT"); | |
} | |
} | |
}, | |
migrateDB: function calmgr_migrateDB(db) { | |
let selectCalendars = createStatement(db, "SELECT * FROM cal_calendars"); | |
let selectPrefs = createStatement(db, "SELECT name, value FROM cal_calendars_prefs WHERE calendar = :calendar"); | |
try { | |
let sortOrder = {}; | |
while (selectCalendars.step()) { | |
let id = cal.getUUID(); // use fresh uuids | |
cal.setPref(getPrefBranchFor(id) + "type", selectCalendars.row.type); | |
cal.setPref(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.step()) { | |
let name = selectPrefs.row.name.toLowerCase(); // may come lower case, so force it to be | |
let value = selectPrefs.row.value; | |
switch (name) { | |
case "readonly": | |
cal.setPref(getPrefBranchFor(id) + "readOnly", value == "true"); | |
break; | |
case "relaxedmode": | |
cal.setPref(getPrefBranchFor(id) + "relaxedMode", value == "true"); | |
break; | |
case "suppressalarms": | |
cal.setPref(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": | |
cal.setPref(getPrefBranchFor(id) + name, value == "true"); | |
break; | |
case "cache.updatetimer": | |
cal.setPref(getPrefBranchFor(id) + "cache.updateTimer", Number(value)); | |
break; | |
case "backup-time": | |
case "uniquenum": | |
cal.setPref(getPrefBranchFor(id) + name, Number(value)); | |
break; | |
case "name": | |
cal.setLocalizedPref(getPrefBranchFor(id) + name, value); | |
break; | |
default: // keep as string | |
cal.setPref(getPrefBranchFor(id) + name, value); | |
break; | |
} | |
} | |
selectPrefs.reset(); | |
} | |
let sortOrderAr = []; | |
for each (let s in sortOrder) { | |
sortOrderAr.push(s); | |
} | |
cal.setPref("calendar.list.sortOrder", sortOrderAr.join(" ")); | |
flushPrefs(); | |
} finally { | |
selectPrefs.reset(); | |
selectCalendars.reset(); | |
} | |
}, | |
checkAndMigrateDB: function calmgr_checkAndMigrateDB() { | |
let dbService = Components.classes["@mozilla.org/storage/service;1"] | |
.getService(Components.interfaces.mozIStorageService); | |
db = dbService.openSpecialDatabase("profile"); | |
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); | |
//cal.LOG("*** Calendar schema version is: " + version); | |
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")) { | |
// 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(); | |
} else { | |
db.rollbackTransaction(); | |
} | |
} catch (exc) { | |
db.rollbackTransaction(); | |
throw exc; | |
} | |
}, | |
/** | |
* @return db schema version | |
* @exception various, depending on error | |
*/ | |
getSchemaVersion: function calMgrGetSchemaVersion(db) { | |
var stmt; | |
var version = null; | |
var 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 = createStatement(db, "SELECT version FROM " + table + " LIMIT 1"); | |
if (stmt.step()) { | |
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 cmgr_alertAndQuit() { | |
// If we're Lightning, we want to include the extension name | |
// in the error message rather than blaming Thunderbird. | |
var errorBoxTitle; | |
var errorBoxText; | |
var errorBoxButtonLabel; | |
var hostAppName = calGetString("brand", "brandShortName", null, "branding"); | |
if (isSunbird()) { | |
errorBoxTitle = calGetString("calendar", "tooNewSchemaErrorBoxTitle", [hostAppName]); | |
errorBoxText = calGetString("calendar", "tooNewSchemaErrorBoxTextSunbird", [hostAppName]); | |
errorBoxButtonLabel = calGetString("calendar", "tooNewSchemaButtonQuit", [hostAppName]); | |
} else { | |
var calAppName = calGetString("lightning", "brandShortName", null, "lightning"); | |
errorBoxTitle = calGetString("calendar", "tooNewSchemaErrorBoxTitle", [calAppName]); | |
errorBoxText = calGetString("calendar", "tooNewSchemaErrorBoxTextLightning", [calAppName, hostAppName]); | |
errorBoxButtonLabel = calGetString("calendar", "tooNewSchemaButtonRestart", [hostAppName]); | |
} | |
var promptSvc = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] | |
.getService(Components.interfaces.nsIPromptService); | |
var errorBoxButtonFlags = (promptSvc.BUTTON_POS_0 * | |
promptSvc.BUTTON_TITLE_IS_STRING + | |
promptSvc.BUTTON_POS_0_DEFAULT); | |
var choice = 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 | |
var startup = Components.classes["@mozilla.org/toolkit/app-startup;1"] | |
.getService(Components.interfaces.nsIAppStartup); | |
if (isSunbird()) { | |
startup.quit(Components.interfaces.nsIAppStartup.eForceQuit); | |
} else { | |
var em = Components.classes["@mozilla.org/extensions/manager;1"] | |
.getService(Components.interfaces.nsIExtensionManager); | |
em.disableItem("{e2fda1a4-762b-4020-b5ad-a41df1933103}"); // i.e. Lightning | |
startup.quit(Components.interfaces.nsIAppStartup.eRestart | | |
Components.interfaces.nsIAppStartup.eForceQuit); | |
} | |
}, | |
notifyObservers: function(functionName, args) { | |
this.mObservers.notify(functionName, args); | |
}, | |
/** | |
* calICalendarManager interface | |
*/ | |
createCalendar: function cmgr_createCalendar(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(); | |
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); | |
let wWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] | |
.getService(Components.interfaces.nsIWindowWatcher); | |
wWatcher.openWindow(null, | |
"chrome://calendar/content/calendar-error-prompt.xul", | |
"_blank", | |
"chrome,dialog=yes,alwaysRaised=yes", | |
paramBlock); | |
return null; | |
} | |
}, | |
registerCalendar: function(calendar) { | |
// bail if this calendar (or one that looks identical to it) is already registered | |
cal.ASSERT(calendar.id === null, "[calCalendarManager::registerCalendar] calendar already registered!", true); | |
this.assureCache(); | |
calendar.id = cal.getUUID(); | |
cal.setPref(getPrefBranchFor(calendar.id) + "type", calendar.type); | |
cal.setPref(getPrefBranchFor(calendar.id) + "uri", calendar.uri.spec); | |
this.setupCalendar(calendar); | |
flushPrefs(); | |
if (!calendar.getProperty("disabled") && calendar.canRefresh) { | |
calendar.refresh(); | |
} | |
this.notifyObservers("onCalendarRegistered", [calendar]); | |
}, | |
setupCalendar: function cmgr_setupCalendar(calendar) { | |
this.mCache[calendar.id] = calendar; | |
// Add an observer to track readonly-mode triggers | |
var 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++; | |
}, | |
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]); | |
let prefService = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch); | |
prefService.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--; | |
}, | |
deleteCalendar: function(calendar) { | |
/* check to see if calendar is unregistered first... */ | |
/* delete the calendar for good */ | |
if (this.mCache && (calendar.id in this.mCache)) { | |
throw "Can't delete a registered calendar"; | |
} | |
this.notifyObservers("onCalendarDeleting", [calendar]); | |
// XXX This is a workaround for bug 351499. We should remove it once | |
// we sort out the whole "delete" vs. "unsubscribe" UI thing. | |
// | |
// We only want to delete the contents of calendars from local | |
// providers (storage and memory). Otherwise we may nuke someone's | |
// calendar stored on a server when all they really wanted to do was | |
// unsubscribe. | |
if (cal.calInstanceOf(calendar, Components.interfaces.calICalendarProvider) && | |
(calendar.type == "storage" || calendar.type == "memory")) { | |
try { | |
calendar.deleteCalendar(calendar, null); | |
} catch (e) { | |
Components.utils.reportError("error purging calendar: " + e); | |
} | |
} | |
}, | |
getCalendars: function cmgr_getCalendars(count) { | |
this.assureCache(); | |
var calendars = []; | |
for each (var cal in this.mCache) { | |
calendars.push(cal); | |
} | |
count.value = calendars.length; | |
return calendars; | |
}, | |
assureCache: function cmgr_assureCache() { | |
if (!this.mCache) { | |
this.mCache = {}; | |
this.mCalObservers = {}; | |
let prefService = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch); | |
let allCals = {}; | |
for each (let key in prefService.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 = cal.getPrefSafe(calBranch + ".type", null); | |
let curi = cal.getPrefSafe(calBranch + ".uri", null); | |
try { | |
if (!ctype || !curi) { // sanity check | |
prefService.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 = 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 each (let calendar in this.mCache) { | |
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); | |
if ( name === "name" ) { | |
return cal.getLocalizedPref(branch, null); | |
} | |
return cal.getPrefSafe(branch, null); | |
}, | |
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); | |
let prefService = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch); | |
// delete before to allow pref-type changes: | |
prefService.deleteBranch(branch); | |
if ( name === "name" ) { | |
cal.setLocalizedPref(branch, value); | |
} else { | |
cal.setPref(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!"); | |
let prefService = Components.classes["@mozilla.org/preferences-service;1"] | |
.getService(Components.interfaces.nsIPrefBranch); | |
prefService.deleteBranch(getPrefBranchFor(calendar.id) + name); | |
}, | |
mObservers: null, | |
addObserver: function(aObserver) { | |
this.mObservers.add(aObserver); | |
}, | |
removeObserver: function(aObserver) { | |
this.mObservers.remove(aObserver); | |
} | |
}; | |
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: function mBL_QueryInterface(aIID) { | |
return doQueryInterface(this, calMgrCalendarObserver.prototype, aIID, | |
[Components.interfaces.nsIWindowMediatorListener, | |
Components.interfaces.calIObserver]); | |
}, | |
// calIObserver: | |
onStartBatch: function() {}, | |
onEndBatch: function() {}, | |
onLoad: function(calendar) {}, | |
onAddItem: function(aItem) {}, | |
onModifyItem: function(aNewItem, aOldItem) {}, | |
onDeleteItem: function(aDeletedItem) {}, | |
onError: function(aCalendar, aErrNo, aMessage) { | |
this.announceError(aCalendar, aErrNo, aMessage); | |
}, | |
onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) { | |
switch (aName) { | |
case "requiresNetwork": | |
this.calMgr.mNetworkCalendarCount += (aValue ? 1 : -1); | |
break; | |
case "readOnly": | |
this.calMgr.mReadonlyCalendarCount += (aValue ? 1 : -1); | |
break; | |
case "cache.enabled": | |
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(); | |
} | |
break; | |
case "disabled": | |
if (!aValue && aCalendar.canRefresh) { | |
aCalendar.refresh(); | |
} | |
break; | |
} | |
}, | |
onPropertyDeleting: function(aCalendar, aName) { | |
this.onPropertyChanged(aCalendar, aName, false, true); | |
}, | |
// Error announcer specific functions | |
announceError: function(aCalendar, aErrNo, aMessage) { | |
var paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"] | |
.createInstance(Components.interfaces.nsIDialogParamBlock); | |
var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"] | |
.getService(Components.interfaces.nsIStringBundleService); | |
var props = sbs.createBundle("chrome://calendar/locale/calendar.properties"); | |
var 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. | |
var 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 (var err in calIErrors) { | |
if (calIErrors[err] == aErrNo) { | |
errCode = err; | |
} | |
} | |
} | |
var 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]); | |
default: | |
message = aMessage; | |
} | |
paramBlock.SetString(0, errMsg); | |
paramBlock.SetString(1, errCode); | |
paramBlock.SetString(2, message); | |
this.storedReadOnly = this.calendar.readOnly; | |
var errorCode = calGetString("calendar","errorCode", [errCode]); | |
var errorDescription = calGetString("calendar","errorDescription", [message]); | |
var summary = errMsg + " " + errorCode + ". " + errorDescription; | |
// Log warnings in error console. | |
// Report serious errors in both error console and in prompt window. | |
var isSerious = (aErrNo == calIErrors.MODIFICATION_FAILED); | |
if (!isSerious) { | |
WARN(summary); | |
} else { | |
// Write error to console. | |
Components.utils.reportError(summary); | |
// silently don't do anything if this message already has | |
// been announced without being acknowledged. | |
if (this.announcedMessages.some( | |
function(element, index, array) { | |
return equalMessage(paramBlock, element); | |
})) { | |
return; | |
} | |
// this message hasn't been announced recently, remember the | |
// details of the message for future reference. | |
this.announcedMessages.push(paramBlock); | |
// Display in prompt window. | |
var wWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] | |
.getService(Components.interfaces.nsIWindowWatcher); | |
var promptWindow = | |
wWatcher.openWindow | |
(null, "chrome://calendar/content/calendar-error-prompt.xul", | |
"_blank", "chrome,dialog=yes,alwaysRaised=yes", | |
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.) | |
var observer = this; | |
function awaitLoad(event) { | |
// #2 loaded, remove load listener | |
promptWindow.removeEventListener("load", awaitLoad, false); | |
function awaitUnload(event) { | |
// #4 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. | |
observer.announcedMessages = | |
observer.announcedMessages.filter(function(msg) { | |
return !equalMessage(msg, paramBlock); | |
}); | |
promptWindow.removeEventListener("unload", awaitUnload, | |
false); | |
} catch (e) { | |
Components.utils.reportError(e); | |
} | |
} | |
// #3 add unload listener (wait for user to close promptWindow) | |
promptWindow.addEventListener("unload", awaitUnload, false); | |
} | |
// #1 add load listener | |
promptWindow.addEventListener("load", awaitLoad, false); | |
} | |
} | |
}; | |
function calDummyCalendar(type) { | |
this.initProviderBase(); | |
this.type = type; | |
} | |
calDummyCalendar.prototype = { | |
__proto__: cal.ProviderBase.prototype, | |
getProperty: function calDummyCalendar_getProperty(aName) { | |
switch (aName) { | |
case "force-disabled": | |
return true; | |
default: | |
return this.__proto__.__proto__.getProperty.apply(this, arguments); | |
} | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ***** BEGIN LICENSE BLOCK ***** | |
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 | |
* | |
* The contents of this file are subject to the Mozilla Public License Version | |
* 1.1 (the "License"); you may not use this file except in compliance with | |
* the License. You may obtain a copy of the License at | |
* http://www.mozilla.org/MPL/ | |
* | |
* Software distributed under the License is distributed on an "AS IS" basis, | |
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | |
* for the specific language governing rights and limitations under the | |
* License. | |
* | |
* The Original Code is Sun Microsystems, Inc. code. | |
* | |
* The Initial Developers of the Original Code are | |
* Philipp Kewisch <[email protected]> | |
* Daniel Boelzle <[email protected]> | |
* Portions created by the Initial Developer are Copyright (C) 2007 | |
* the Initial Developer. All Rights Reserved. | |
* | |
* Contributor(s): | |
* | |
* Alternatively, the contents of this file may be used under the terms of | |
* either the GNU General Public License Version 2 or later (the "GPL"), or | |
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), | |
* in which case the provisions of the GPL or the LGPL are applicable instead | |
* of those above. If you wish to allow use of your version of this file only | |
* under the terms of either the GPL or the LGPL, and not to allow others to | |
* use your version of this file under the terms of the MPL, indicate your | |
* decision by deleting the provisions above and replace them with the notice | |
* and other provisions required by the GPL or the LGPL. If you do not delete | |
* the provisions above, a recipient may use your version of this file under | |
* the terms of any one of the MPL, the GPL or the LGPL. | |
* | |
* ***** END LICENSE BLOCK ***** */ | |
Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); | |
function calCachedCalendarObserverHelper(home, isCachedObserver) { | |
this.home = home; | |
this.isCachedObserver = isCachedObserver; | |
} | |
calCachedCalendarObserverHelper.prototype = { | |
isCachedObserver: false, | |
onStartBatch: function() { | |
this.home.mObservers.notify("onStartBatch"); | |
}, | |
onEndBatch: function() { | |
this.home.mObservers.notify("onEndBatch"); | |
}, | |
onLoad: function(calendar) { | |
if (this.isCachedObserver) { | |
this.home.mObservers.notify("onLoad", [this.home]); | |
} else { | |
// start sync action after uncached calendar has been loaded. | |
// xxx todo, think about: | |
// although onAddItem et al have been called, we need to fire | |
// an additional onLoad completing the refresh call (->composite) | |
var home = this.home; | |
home.synchronize( | |
function(status) { | |
home.mObservers.notify("onLoad", [home]); | |
}); | |
} | |
}, | |
onAddItem: function(aItem) { | |
if (this.isCachedObserver) { | |
this.home.mObservers.notify("onAddItem", arguments); | |
} | |
}, | |
onModifyItem: function(aNewItem, aOldItem) { | |
if (this.isCachedObserver) { | |
this.home.mObservers.notify("onModifyItem", arguments); | |
} | |
}, | |
onDeleteItem: function(aItem) { | |
if (this.isCachedObserver) { | |
this.home.mObservers.notify("onDeleteItem", arguments); | |
} | |
}, | |
onError: function(aCalendar, aErrNo, aMessage) { | |
this.home.mObservers.notify("onError", arguments); | |
}, | |
onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) { | |
if (!this.isCachedObserver) { | |
this.home.mObservers.notify("onPropertyChanged", [this.home, aName, aValue, aOldValue]); | |
} | |
}, | |
onPropertyDeleting: function(aCalendar, aName) { | |
if (!this.isCachedObserver) { | |
this.home.mObservers.notify("onPropertyDeleting", [this.home, aName]); | |
} | |
} | |
}; | |
function calCachedCalendar(uncachedCalendar) { | |
this.wrappedJSObject = this; | |
this.mSyncQueue = []; | |
this.mObservers = new cal.ObserverBag(Components.interfaces.calIObserver); | |
uncachedCalendar.superCalendar = this; | |
uncachedCalendar.addObserver(new calCachedCalendarObserverHelper(this, false)); | |
this.mUncachedCalendar = uncachedCalendar; | |
this.setupCachedCalendar(); | |
if (this.supportsChangeLog) { | |
var updateTimer = this.getProperty("cache.updateTimer"); | |
if (updateTimer === null) { | |
updateTimer = 4; // override for changelog based providers | |
} | |
var timerCallback = { | |
mCalendar: this, | |
notify: function(timer) { | |
LOG("[calCachedCalendar] replay timer"); | |
if (!this.mCalendar.getProperty("disabled")) { | |
this.mCalendar.refresh(); | |
} | |
} | |
}; | |
this.mReplayTimer = Components.classes["@mozilla.org/timer;1"] | |
.createInstance(Components.interfaces.nsITimer); | |
this.mReplayTimer.initWithCallback(timerCallback, | |
updateTimer * 60 * 1000, | |
Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); | |
} | |
} | |
calCachedCalendar.prototype = { | |
QueryInterface: function cCC_QueryInterface(aIID) { | |
if (aIID.equals(Components.interfaces.calISchedulingSupport)) { | |
// check whether uncached calendar supports it: | |
if (this.mUncachedCalendar.QueryInterface(aIID)) { | |
return this; | |
} | |
} | |
return doQueryInterface(this, calCachedCalendar.prototype, aIID, | |
[Components.interfaces.calICalendar, | |
Components.interfaces.nsISupports]); | |
}, | |
mCachedCalendar: null, | |
mCachedObserver: null, | |
mUncachedCalendar: null, | |
mObservers: null, | |
mSuperCalendar: null, | |
mReplayTimer: null, | |
onCalendarUnregistering: function() { | |
if (this.mReplayTimer) { | |
this.mReplayTimer.cancel(); | |
this.mReplayTimer = null; | |
} | |
if (this.mCachedCalendar) { | |
this.mCachedCalendar.removeObserver(this.mCachedObserver); | |
// Although this doesn't really follow the spec, we know the | |
// storage calendar's deleteCalendar method is synchronous. | |
// TODO put changes into a different calendar and delete | |
// afterwards. | |
this.mCachedCalendar.QueryInterface(Components.interfaces.calICalendarProvider) | |
.deleteCalendar(this.mCachedCalendar, null); | |
this.mCachedCalendar = null; | |
} | |
}, | |
setupCachedCalendar: function cCC_setupCachedCalendar() { | |
try { | |
if (this.mCachedCalendar) { // this is actually a resetupCachedCalendar: | |
// Although this doesn't really follow the spec, we know the | |
// storage calendar's deleteCalendar method is synchronous. | |
// TODO put changes into a different calendar and delete | |
// afterwards. | |
this.mCachedCalendar.QueryInterface(Components.interfaces.calICalendarProvider) | |
.deleteCalendar(this.mCachedCalendar, null); | |
if (this.supportsChangeLog) { | |
// start with full sync: | |
this.mUncachedCalendar.resetLog(); | |
} | |
} else { | |
let calType = getPrefSafe("calendar.cache.type", "storage"); | |
// While technically, the above deleteCalendar should delete the | |
// whole calendar, this is nothing more than deleting all events | |
// todos and properties. Therefore the initialization can be | |
// skipped. | |
let cachedCalendar = Components.classes["@mozilla.org/calendar/calendar;1?type=" + calType] | |
.createInstance(Components.interfaces.calICalendar); | |
switch (calType) { | |
case "memory": | |
if (this.supportsChangeLog) { | |
// start with full sync: | |
this.mUncachedCalendar.resetLog(); | |
} | |
break; | |
case "storage": | |
let file = getCalendarDirectory(); | |
file.append("cache.sqlite"); | |
cachedCalendar.uri = getIOService().newFileURI(file); | |
cachedCalendar.id = this.id; | |
break; | |
default: | |
throw new Error("unsupported cache calendar type: " + calType); | |
} | |
cachedCalendar.transientProperties = true; | |
cachedCalendar.setProperty("relaxedMode", true); | |
cachedCalendar.superCalendar = this; | |
if (!this.mCachedObserver) { | |
this.mCachedObserver = new calCachedCalendarObserverHelper(this, true); | |
} | |
cachedCalendar.addObserver(this.mCachedObserver); | |
this.mCachedCalendar = cachedCalendar; | |
} | |
} catch (exc) { | |
Components.utils.reportError(exc); | |
} | |
}, | |
mPendingSync: null, | |
mSyncQueue: null, | |
synchronize: function cCC_synchronize(respFunc) { | |
this.mSyncQueue.push(respFunc); | |
if (this.mSyncQueue.length > 1) { // don't use mPendingSync here | |
LOG("[calCachedCalendar] sync in action/pending."); | |
return this.mPendingSync; | |
} | |
var this_ = this; | |
function emptyQueue(status) { | |
var queue = this_.mSyncQueue; | |
this_.mSyncQueue = []; | |
function execResponseFunc(func) { | |
try { | |
func(status); | |
} catch (exc) { | |
ASSERT(false, exc); | |
} | |
} | |
queue.forEach(execResponseFunc); | |
LOG("[calCachedCalendar] sync queue empty."); | |
var op = this_.mPendingSync; | |
this_.mPendingSync = null; | |
return op; | |
} | |
if (this.offline) { | |
return emptyQueue(Components.results.NS_OK); | |
} | |
if (this.supportsChangeLog) { | |
LOG("[calCachedCalendar] Doing changelog based sync for calendar " + this.uri.spec); | |
var opListener = { | |
onResult: function(op, result) { | |
if (!op || !op.isPending) { | |
var status = (op ? op.status : Components.results.NS_OK); | |
ASSERT(Components.isSuccessCode(status), "replay action failed: " + (op ? op.id : "<unknown>")); | |
LOG("[calCachedCalendar] replayChangesOn finished."); | |
emptyQueue(status); | |
} | |
} | |
}; | |
this.mPendingSync = this.mUncachedCalendar.replayChangesOn(this.mCachedCalendar, opListener); | |
return this.mPendingSync; | |
} | |
LOG("[calCachedCalendar] Doing full sync for calendar " + this.uri.spec); | |
// TODO put changes into a different calendar and delete | |
// afterwards. | |
var completeListener = { | |
hasRenewedCalendar: false, | |
onGetResult: function cCC_oOC_cL_onGetResult(aCalendar, | |
aStatus, | |
aItemType, | |
aDetail, | |
aCount, | |
aItems) { | |
if (Components.isSuccessCode(aStatus)) { | |
if (!this.hasRenewedCalendar) { | |
// TODO instead of deleting the calendar and creating a new | |
// one, maybe we want to do a "real" sync between the | |
// existing local calendar and the remote calendar. | |
this_.setupCachedCalendar(); | |
this.hasRenewedCalendar = true; | |
} | |
for each (var item in aItems) { | |
this_.mCachedCalendar.addItem(item, null); | |
} | |
} | |
}, | |
onOperationComplete: function cCC_oOC_cL_onOperationComplete(aCalendar, | |
aStatus, | |
aOpType, | |
aId, | |
aDetail) { | |
ASSERT(Components.isSuccessCode(aStatus), "getItems failed: " + aStatus); | |
emptyQueue(aStatus); | |
} | |
}; | |
this.mPendingSync = this.mUncachedCalendar.getItems(Components.interfaces.calICalendar.ITEM_FILTER_ALL_ITEMS, | |
0, null, null, completeListener); | |
return this.mPendingSync; | |
}, | |
onOfflineStatusChanged: function cCC_onOfflineStatusChanged(aNewState) { | |
if (aNewState) { | |
// Going offline: (XXX get items before going offline?) => we may ask the user to stay online a bit longer | |
} else { | |
// Going online (start replaying changes to the remote calendar) | |
this.refresh(); | |
} | |
}, | |
get superCalendar() { | |
return this.mSuperCalendar && this.mSuperCalendar.superCalendar || this; | |
}, | |
set superCalendar(val) { | |
return (this.mSuperCalendar = val); | |
}, | |
get offline() { | |
return getIOService().offline; | |
}, | |
get supportsChangeLog() { | |
return calInstanceOf(this.mUncachedCalendar, Components.interfaces.calIChangeLog); | |
}, | |
get canRefresh() { // enable triggering sync using the reload button | |
return true; | |
}, | |
refresh: function() { | |
if (this.mUncachedCalendar.canRefresh && !this.offline) { | |
return this.mUncachedCalendar.refresh(); // will trigger synchronize once the calendar is loaded | |
} else { | |
var this_ = this; | |
return this.synchronize( | |
function(status) { // fire completing onLoad for this refresh call | |
this_.mCachedObserver.onLoad(this_.mCachedCalendar); | |
}); | |
} | |
}, | |
addObserver: function(aObserver) { | |
this.mObservers.add(aObserver); | |
}, | |
removeObserver: function(aObserver) { | |
this.mObservers.remove(aObserver); | |
}, | |
addItem: function(item, listener) { | |
return this.adoptItem(item.clone(), listener); | |
}, | |
adoptItem: function(item, listener) { | |
if (this.offline) { | |
ASSERT(false, "unexpected!"); | |
if (listener) { | |
listener.onOperationComplete(this, Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperation.ADD, null, null); | |
} | |
return null; | |
} | |
// Forwarding add/modify/delete to the cached calendar using the calIObserver | |
// callbacks would be advantageous, because the uncached provider could implement | |
// a true push mechanism firing without being triggered from within the program. | |
// But this would mean the uncached provider fires on the passed | |
// calIOperationListener, e.g. *before* it fires on calIObservers | |
// (because that order is undefined). Firing onOperationComplete before onAddItem et al | |
// would result in this facade firing onOperationComplete even though the modification | |
// hasn't yet been performed on the cached calendar (which happens in onAddItem et al). | |
// Result is that we currently stick to firing onOperationComplete if the cached calendar | |
// has performed the modification, see below: | |
var this_ = this; | |
var opListener = { | |
onGetResult: function(calendar, status, itemType, detail, count, items) { | |
ASSERT(false, "unexpected!"); | |
}, | |
onOperationComplete: function(calendar, status, opType, id, detail) { | |
if (Components.isSuccessCode(status)) { | |
this_.mCachedCalendar.addItem(detail, listener); | |
} else if (listener) { | |
listener.onOperationComplete(this_, status, opType, id, detail); | |
} | |
} | |
} | |
return this.mUncachedCalendar.adoptItem(item, opListener); | |
}, | |
modifyItem: function(newItem, oldItem, listener) { | |
if (this.offline) { | |
ASSERT(false, "unexpected!"); | |
if (listener) { | |
listener.onOperationComplete(this, Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperation.MODIFY, null, null); | |
} | |
return null; | |
} | |
// Forwarding add/modify/delete to the cached calendar using the calIObserver | |
// callbacks would be advantageous, because the uncached provider could implement | |
// a true push mechanism firing without being triggered from within the program. | |
// But this would mean the uncached provider fires on the passed | |
// calIOperationListener, e.g. *before* it fires on calIObservers | |
// (because that order is undefined). Firing onOperationComplete before onAddItem et al | |
// would result in this facade firing onOperationComplete even though the modification | |
// hasn't yet been performed on the cached calendar (which happens in onAddItem et al). | |
// Result is that we currently stick to firing onOperationComplete if the cached calendar | |
// has performed the modification, see below: | |
var this_ = this; | |
var opListener = { | |
onGetResult: function(calendar, status, itemType, detail, count, items) { | |
ASSERT(false, "unexpected!"); | |
}, | |
onOperationComplete: function(calendar, status, opType, id, detail) { | |
if (Components.isSuccessCode(status)) { | |
this_.mCachedCalendar.modifyItem(detail, oldItem, listener); | |
} else if (listener) { | |
listener.onOperationComplete(this_, status, opType, id, detail); | |
} | |
} | |
} | |
return this.mUncachedCalendar.modifyItem(newItem, oldItem, opListener); | |
}, | |
deleteItem: function(item, listener) { | |
/* if (this.offline) { | |
ASSERT(false, "unexpected!"); | |
if (listener) { | |
listener.onOperationComplete(this, Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperation.DELETE, null, null); | |
} | |
return null; | |
}*/ | |
// Forwarding add/modify/delete to the cached calendar using the calIObserver | |
// callbacks would be advantageous, because the uncached provider could implement | |
// a true push mechanism firing without being triggered from within the program. | |
// But this would mean the uncached provider fires on the passed | |
// calIOperationListener, e.g. *before* it fires on calIObservers | |
// (because that order is undefined). Firing onOperationComplete before onAddItem et al | |
// would result in this facade firing onOperationComplete even though the modification | |
// hasn't yet been performed on the cached calendar (which happens in onAddItem et al). | |
// Result is that we currently stick to firing onOperationComplete if the cached calendar | |
// has performed the modification, see below: | |
var this_ = this; | |
var opListener = { | |
onGetResult: function(calendar, status, itemType, detail, count, items) { | |
ASSERT(false, "unexpected!"); | |
}, | |
onOperationComplete: function(calendar, status, opType, id, detail) { | |
if (Components.isSuccessCode(status)) { | |
this_.mCachedCalendar.deleteItem(item, listener); | |
} else if (listener) { | |
listener.onOperationComplete(this_, status, opType, id, detail); | |
} | |
} | |
} | |
return this.mCachedCalendar.deleteItem(item, null); | |
//return this.mUncachedCalendar.deleteItem(item, opListener); | |
} | |
}; | |
(function() { | |
function defineForwards(proto, targetName, functions, getters, gettersAndSetters) { | |
function defineForwardGetter(attr) { | |
proto.__defineGetter__(attr, function() { return this[targetName][attr]; }); | |
} | |
function defineForwardGetterAndSetter(attr) { | |
defineForwardGetter(attr); | |
proto.__defineSetter__(attr, function(value) { return (this[targetName][attr] = value); }); | |
} | |
function defineForwardFunction(funcName) { | |
proto[funcName] = function() { | |
var obj = this[targetName]; | |
return obj[funcName].apply(obj, arguments); | |
}; | |
} | |
functions.forEach(defineForwardFunction); | |
getters.forEach(defineForwardGetter); | |
gettersAndSetters.forEach(defineForwardGetterAndSetter); | |
} | |
defineForwards(calCachedCalendar.prototype, "mUncachedCalendar", | |
["getProperty", "setProperty", "deleteProperty", | |
"isInvitation", "getInvitedAttendee", "canNotify"], | |
["type"], | |
["id", "name", "uri", "readOnly"]); | |
defineForwards(calCachedCalendar.prototype, "mCachedCalendar", | |
["getItem", "getItems", "startBatch", "endBatch"], [], []); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* ***** BEGIN LICENSE BLOCK ***** | |
* Version: MPL 1.1/GPL 2.0/LGPL 2.1 | |
* | |
* The contents of this file are subject to the Mozilla Public License Version | |
* 1.1 (the "License"); you may not use this file except in compliance with | |
* the License. You may obtain a copy of the License at | |
* http://www.mozilla.org/MPL/ | |
* | |
* Software distributed under the License is distributed on an "AS IS" basis, | |
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | |
* for the specific language governing rights and limitations under the | |
* License. | |
* | |
* The Original Code is Oracle Corporation code. | |
* | |
* The Initial Developer of the Original Code is | |
* Oracle Corporation | |
* Portions created by the Initial Developer are Copyright (C) 2005, 2006 | |
* the Initial Developer. All Rights Reserved. | |
* | |
* Contributor(s): | |
* Vladimir Vukicevic <[email protected]> | |
* Joey Minta <[email protected]> | |
* Dan Mosedale <[email protected]> | |
* Thomas Benisch <[email protected]> | |
* Matthew Willis <[email protected]> | |
* Philipp Kewisch <[email protected]> | |
* Daniel Boelzle <[email protected]> | |
* Sebastian Schwieger <[email protected]> | |
* Fred Jendrzejewski <[email protected]> | |
* | |
* Alternatively, the contents of this file may be used under the terms of | |
* either the GNU General Public License Version 2 or later (the "GPL"), or | |
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), | |
* in which case the provisions of the GPL or the LGPL are applicable instead | |
* of those above. If you wish to allow use of your version of this file only | |
* under the terms of either the GPL or the LGPL, and not to allow others to | |
* use your version of this file under the terms of the MPL, indicate your | |
* decision by deleting the provisions above and replace them with the notice | |
* and other provisions required by the GPL or the LGPL. If you do not delete | |
* the provisions above, a recipient may use your version of this file under | |
* the terms of any one of the MPL, the GPL or the LGPL. | |
* | |
* ***** END LICENSE BLOCK ***** */ | |
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); | |
Components.utils.import("resource://gre/modules/Services.jsm"); | |
Components.utils.import("resource://calendar/modules/calUtils.jsm"); | |
Components.utils.import("resource://calendar/modules/calAlarmUtils.jsm"); | |
Components.utils.import("resource://calendar/modules/calProviderUtils.jsm"); | |
Components.utils.import("resource://calendar/modules/calStorageUpgrade.jsm"); | |
Components.utils.import("resource://calendar/modules/calStorageHelpers.jsm"); | |
const USECS_PER_SECOND = 1000000; | |
const kCalICalendar = Components.interfaces.calICalendar; | |
// | |
// calStorageCalendar | |
// | |
function calStorageCalendar() { | |
this.initProviderBase(); | |
this.mItemCache = {}; | |
this.mRecEventCache = {}; | |
this.mRecTodoCache = {}; | |
} | |
calStorageCalendar.prototype = { | |
__proto__: cal.ProviderBase.prototype, | |
classID: Components.ID("{b3eaa1c4-5dfe-4c0a-b62a-b3a514218461}"), | |
contractID: "@mozilla.org/calendar/calendar;1?type=storage", | |
classDescription: "Calendar Storage Provider", | |
getInterfaces: function (count) { | |
let ifaces = [ | |
Components.interfaces.nsISupports, | |
Components.interfaces.calICalendarManager, | |
Components.interfaces.calIStartupService, | |
Components.interfaces.nsIObserver, | |
Components.interfaces.nsIClassInfo | |
]; | |
count.value = ifaces.length; | |
return ifaces; | |
}, | |
getHelperForLanguage: function (language) { | |
return null; | |
}, | |
implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT, | |
flags: 0, | |
// | |
// private members | |
// | |
mDB: null, | |
mItemCache: null, | |
mRecItemCacheInited: false, | |
mRecEventCache: null, | |
mRecTodoCache: null, | |
mLastStatement: null, | |
// | |
// nsISupports interface | |
// | |
QueryInterface: function (aIID) { | |
return doQueryInterface(this, calStorageCalendar.prototype, aIID, | |
[Components.interfaces.calICalendarProvider, | |
Components.interfaces.calISyncWriteCalendar]); | |
}, | |
// | |
// calICalendarProvider interface | |
// | |
get prefChromeOverlay() { | |
return null; | |
}, | |
get displayName() { | |
return calGetString("calendar", "storageName"); | |
}, | |
createCalendar: function cSC_createCalendar() { | |
throw NS_ERROR_NOT_IMPLEMENTED; | |
}, | |
deleteCalendar: function cSC_deleteCalendar(cal, listener) { | |
cal = cal.wrappedJSObject; | |
for each (let stmt in this.mDeleteEventExtras) { | |
try { | |
this.prepareStatement(stmt); | |
stmt.execute(); | |
} finally { | |
stmt.reset(); | |
} | |
} | |
for each (let stmt in this.mDeleteTodoExtras) { | |
try { | |
this.prepareStatement(stmt); | |
stmt.execute(); | |
} finally { | |
stmt.reset(); | |
} | |
} | |
try { | |
this.prepareStatement(this.mDeleteAllEvents); | |
this.mDeleteAllEvents.execute(); | |
} finally { | |
this.mDeleteAllEvents.reset(); | |
} | |
try { | |
this.prepareStatement(this.mDeleteAllTodos); | |
this.mDeleteAllTodos.execute(); | |
} finally { | |
this.mDeleteAllTodos.reset(); | |
} | |
try { | |
this.prepareStatement(this.mDeleteAllMetaData); | |
this.mDeleteAllMetaData.execute(); | |
} finally { | |
this.mDeleteAllMetaData.reset(); | |
} | |
try { | |
if (listener) { | |
listener.onDeleteCalendar(cal, Components.results.NS_OK, null); | |
} | |
} catch (ex) { | |
this.logError("error calling listener.onDeleteCalendar", ex); | |
} | |
}, | |
mRelaxedMode: undefined, | |
get relaxedMode() { | |
if (this.mRelaxedMode === undefined) { | |
this.mRelaxedMode = this.getProperty("relaxedMode"); | |
} | |
return this.mRelaxedMode; | |
}, | |
// | |
// calICalendar interface | |
// | |
getProperty: function cSC_getProperty(aName) { | |
switch (aName) { | |
case "cache.supported": | |
return false; | |
case "requiresNetwork": | |
return false; | |
} | |
return this.__proto__.__proto__.getProperty.apply(this, arguments); | |
}, | |
// readonly attribute AUTF8String type; | |
get type() { return "storage"; }, | |
// attribute AUTF8String id; | |
get id() { | |
return this.__proto__.__proto__.__lookupGetter__("id").call(this); | |
}, | |
set id(val) { | |
let id = this.__proto__.__proto__.__lookupSetter__("id").call(this, val); | |
if (!this.mDB && this.uri && this.id) { | |
// Prepare the database as soon as we have an id and an uri. | |
this.prepareInitDB(); | |
} | |
return id; | |
}, | |
// attribute nsIURI uri; | |
get uri() { | |
return this.__proto__.__proto__.__lookupGetter__("uri").call(this); | |
}, | |
set uri(aUri) { | |
// We can only load once | |
if (this.uri) { | |
throw Components.results.NS_ERROR_FAILURE; | |
} | |
let uri = this.__proto__.__proto__.__lookupSetter__("uri").call(this, aUri); | |
if (!this.mDB && this.uri && this.id) { | |
// Prepare the database as soon as we have an id and an uri. | |
this.prepareInitDB(); | |
} | |
return uri; | |
}, | |
/** | |
* Initialize the Database. This should only be called from the uri or id | |
* setter and requires those two attributes to be set. | |
*/ | |
prepareInitDB: function cSC_prepareInitDB() { | |
if (this.uri.schemeIs("file")) { | |
let fileURL = this.uri.QueryInterface(Components.interfaces.nsIFileURL); | |
if (!fileURL) | |
throw Components.results.NS_ERROR_NOT_IMPLEMENTED; | |
// open the database | |
this.mDB = Services.storage.openDatabase(fileURL.file); | |
upgradeDB(this.mDB); | |
} else if (this.uri.schemeIs("moz-profile-calendar")) { | |
// This is an old-style moz-profile-calendar. It requires some | |
// migration steps. | |
let localDB = cal.getCalendarDirectory(); | |
localDB.append("local.sqlite"); | |
localDB = Services.storage.openDatabase(localDB); | |
// First, we need to check if this is from 0.9, i.e we need to | |
// migrate from storage.sdb to local.sqlite. | |
let storageSdb = Services.dirsvc.get("ProfD", Components.interfaces.nsILocalFile); | |
storageSdb.append("storage.sdb"); | |
this.mDB = Services.storage.openDatabase(storageSdb); | |
if (this.mDB.tableExists("cal_events")) { | |
cal.LOG("Storage: Migrating storage.sdb -> local.sqlite"); | |
upgradeDB(this.mDB); // upgrade schema before migating data | |
let attachStatement = createStatement(this.mDB, "ATTACH DATABASE :file_path AS local_sqlite"); | |
try { | |
attachStatement.params.file_path = localDB.databaseFile.path; | |
attachStatement.execute(); | |
} catch (exc) { | |
this.logError("prepareInitDB attachStatement.execute exception", exc); | |
throw exc; | |
} finally { | |
attachStatement.reset(); | |
} | |
try { | |
// hold lock on storage.sdb until we've migrated data from storage.sdb: | |
this.mDB.beginTransactionAs(Components.interfaces.mozIStorageConnection.TRANSACTION_EXCLUSIVE); | |
try { | |
if (this.mDB.tableExists("cal_events")) { // check again (with lock) | |
// take over data and drop from storage.sdb tables: | |
for (let table in getSqlTable(DB_SCHEMA_VERSION)) { | |
if (table.substr(0, 4) != "idx_") { | |
this.mDB.executeSimpleSQL("CREATE TABLE local_sqlite." + table + | |
" AS SELECT * FROM " + table + | |
"; DROP TABLE IF EXISTS " + table); | |
} | |
} | |
this.mDB.commitTransaction(); | |
} else { // migration done in the meantime | |
this.mDB.rollbackTransaction(); | |
} | |
} catch (exc) { | |
this.logError("prepareInitDB storage.sdb migration exception", exc); | |
this.mDB.rollbackTransaction(); | |
throw exc; | |
} | |
} finally { | |
this.mDB.executeSimpleSQL("DETACH DATABASE local_sqlite"); | |
} | |
} | |
// Now that we are through, set the database to the new local.sqlite | |
// and start the upgraders. | |
this.mDB = localDB; | |
upgradeDB(this.mDB); | |
// Afterwards, we have to migrate the moz-profile-calendars to the | |
// new moz-storage-calendar schema. This is needed due to bug 479867 | |
// and its regression bug 561735. The first calendar created before | |
// v19 already has a moz-profile-calendar:// uri without an ?id= | |
// parameter (the id in the databse is 0). We need to migrate this | |
// special calendar differently. | |
// WARNING: This is a somewhat fragile process. Great care should be | |
// taken during future schema upgrades to make sure this still | |
// works. | |
this.mDB.beginTransactionAs(Components.interfaces.mozIStorageConnection.TRANSACTION_EXCLUSIVE); | |
try { | |
/** | |
* Helper function to migrate all tables from one id to the next | |
* | |
* @param db The database to use | |
* @param newCalId The new calendar id to set | |
* @param oldCalId The old calendar id to look for | |
*/ | |
function migrateTables(db, newCalId, oldCalId) { | |
for each (let tbl in ["cal_alarms", "cal_attachments", | |
"cal_attendees", "cal_events", | |
"cal_metadata", "cal_properties", | |
"cal_recurrence", "cal_relations", | |
"cal_todos"]) { | |
let stmt; | |
try { | |
stmt = createStatement(db, "UPDATE " + tbl + | |
" SET cal_id = :cal_id" + | |
" WHERE cal_id = :old_cal_id"); | |
stmt.params.cal_id = newCalId; | |
stmt.params.old_cal_id = oldCalId; | |
stmt.execute(); | |
} catch (e) { | |
// Pass error through to enclosing try/catch block | |
throw e; | |
} finally { | |
if (stmt) { | |
stmt.reset(); | |
} | |
} | |
} | |
} | |
let id = 0; | |
let path = this.uri.path; | |
let pos = path.indexOf("?id="); | |
if (pos != -1) { | |
// There is an "id" parameter in the uri. This calendar | |
// has not been migrated to using the uuid as its cal_id. | |
pos = this.uri.path.indexOf("?id="); | |
if (pos != -1) { | |
cal.LOG("Storage: Migrating numeric cal_id to uuid"); | |
id = parseInt(path.substr(pos + 4), 10); | |
migrateTables(this.mDB, this.id, id); | |
// Now remove the id from the uri to make sure we don't do this | |
// again. Remeber the id, so we can recover in case something | |
// goes wrong. | |
this.setProperty("uri", "moz-storage-calendar://"); | |
this.setProperty("old_calendar_id", id); | |
this.mDB.commitTransaction(); | |
} else { | |
this.mDB.rollbackTransaction(); | |
} | |
} else { | |
// For some reason, the first storage calendar before the | |
// v19 upgrade has cal_id=0. If we still have a | |
// moz-profile-calendar here, then this is the one and we | |
// need to move all events with cal_id=0 to this id. | |
cal.LOG("Storage: Migrating stray cal_id=0 calendar to uuid"); | |
migrateTables(this.mDB, this.id, 0); | |
this.setProperty("uri", "moz-storage-calendar://"); | |
this.setProperty("old_calendar_id", 0); | |
this.mDB.commitTransaction(); | |
} | |
} catch (exc) { | |
this.logError("prepareInitDB moz-profile-calendar migration exception", exc); | |
this.mDB.rollbackTransaction(); | |
throw exc; | |
} | |
} else if (this.uri.schemeIs("moz-storage-calendar")) { | |
// New style uri, no need for migration here | |
let localDB = cal.getCalendarDirectory(); | |
localDB.append("local.sqlite"); | |
localDB = Services.storage.openDatabase(localDB); | |
this.mDB = localDB; | |
upgradeDB(this.mDB); | |
} | |
this.initDB(); | |
}, | |
/** | |
* Takes care of necessary preparations for most of our statements. | |
* | |
* @param aStmt The statement to prepare. | |
*/ | |
prepareStatement: function cSC_prepareStatement(aStmt) { | |
try { | |
aStmt.params.cal_id = this.id; | |
this.mLastStatement = aStmt; | |
} catch (e) { | |
this.logError("prepareStatement exception", e); | |
} | |
}, | |
refresh: function cSC_refresh() { | |
// no-op | |
}, | |
// void addItem( in calIItemBase aItem, in calIOperationListener aListener ); | |
addItem: function cSC_addItem(aItem, aListener) { | |
let newItem = aItem.clone(); | |
return this.adoptItem(newItem, aListener); | |
}, | |
// void adoptItem( in calIItemBase aItem, in calIOperationListener aListener ); | |
adoptItem: function cSC_adoptItem(aItem, aListener) { | |
if (this.readOnly) { | |
this.notifyOperationComplete(aListener, | |
Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperationListener.ADD, | |
null, | |
"Calendar is readonly"); | |
return; | |
} | |
if (aItem.id == null) { | |
// is this an error? Or should we generate an IID? | |
aItem.id = getUUID(); | |
} else { | |
var olditem = this.getItemById(aItem.id); | |
if (olditem) { | |
if (this.relaxedMode) { | |
// we possibly want to interact with the user before deleting | |
this.deleteItemById(aItem.id); | |
} else { | |
this.notifyOperationComplete(aListener, | |
Components.interfaces.calIErrors.DUPLICATE_ID, | |
Components.interfaces.calIOperationListener.ADD, | |
aItem.id, | |
"ID already exists for addItem"); | |
return; | |
} | |
} | |
} | |
let parentItem = aItem.parentItem; | |
if (parentItem != aItem) { | |
parentItem = parentItem.clone(); | |
parentItem.recurrenceInfo.modifyException(aItem, true); | |
} | |
parentItem.calendar = this.superCalendar; | |
parentItem.makeImmutable(); | |
this.flushItem(parentItem, null); | |
// notify the listener | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.ADD, | |
aItem.id, | |
aItem); | |
// notify observers | |
this.observers.notify("onAddItem", [aItem]); | |
}, | |
// void modifyItem( in calIItemBase aNewItem, in calIItemBase aOldItem, in calIOperationListener aListener ); | |
modifyItem: function cSC_modifyItem(aNewItem, aOldItem, aListener) { | |
if (this.readOnly) { | |
this.notifyOperationComplete(aListener, | |
Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperationListener.MODIFY, | |
null, | |
"Calendar is readonly"); | |
return null; | |
} | |
if (!aNewItem) { | |
throw Components.results.NS_ERROR_INVALID_ARG; | |
} | |
var this_ = this; | |
function reportError(errStr, errId) { | |
this_.notifyOperationComplete(aListener, | |
errId ? errId : Components.results.NS_ERROR_FAILURE, | |
Components.interfaces.calIOperationListener.MODIFY, | |
aNewItem.id, | |
errStr); | |
return null; | |
} | |
if (aNewItem.id == null) { | |
// this is definitely an error | |
return reportError("ID for modifyItem item is null"); | |
} | |
// Ensure that we're looking at the base item if we were given an | |
// occurrence. Later we can optimize this. | |
var modifiedItem = aNewItem.parentItem.clone(); | |
if (aNewItem.parentItem != aNewItem) { | |
modifiedItem.recurrenceInfo.modifyException(aNewItem, false); | |
} | |
if (this.relaxedMode) { | |
if (!aOldItem) { | |
aOldItem = this.getItemById(aNewItem.id) || aNewItem; | |
} | |
aOldItem = aOldItem.parentItem; | |
} else { | |
var storedOldItem = (aOldItem ? this.getItemById(aOldItem.id) : null); | |
if (!aOldItem || !storedOldItem) { | |
// no old item found? should be using addItem, then. | |
return reportError("ID does not already exist for modifyItem"); | |
} | |
aOldItem = aOldItem.parentItem; | |
if (aOldItem.generation != storedOldItem.generation) { | |
return reportError("generation too old for for modifyItem"); | |
} | |
// xxx todo: this only modified master item's generation properties | |
// I start asking myself why we need a separate X-MOZ-GENERATION. | |
// Just for the sake of checking inconsistencies of modifyItem calls? | |
if (aOldItem.generation == modifiedItem.generation) { // has been cloned and modified | |
// Only take care of incrementing the generation if relaxed mode is | |
// off. Users of relaxed mode need to take care of this themselves. | |
modifiedItem.generation += 1; | |
} | |
} | |
modifiedItem.makeImmutable(); | |
this.flushItem (modifiedItem, aOldItem); | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.MODIFY, | |
modifiedItem.id, | |
modifiedItem); | |
// notify observers | |
this.observers.notify("onModifyItem", [modifiedItem, aOldItem]); | |
return null; | |
}, | |
// void deleteItem( in string id, in calIOperationListener aListener ); | |
deleteItem: function cSC_deleteItem(aItem, aListener) { | |
if (this.readOnly) { | |
this.notifyOperationComplete(aListener, | |
Components.interfaces.calIErrors.CAL_IS_READONLY, | |
Components.interfaces.calIOperationListener.DELETE, | |
null, | |
"Calendar is readonly"); | |
return; | |
} | |
if (aItem.parentItem != aItem) { | |
aItem.parentItem.recurrenceInfo.removeExceptionFor(aItem.recurrenceId); | |
// xxx todo: would we want to support this case? Removing an occurrence currently results | |
// in a modifyItem(parent) | |
return; | |
} | |
if (aItem.id == null) { | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_ERROR_FAILURE, | |
Components.interfaces.calIOperationListener.DELETE, | |
null, | |
"ID is null for deleteItem"); | |
return; | |
} | |
this.deleteItemById(aItem.id); | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.DELETE, | |
aItem.id, | |
aItem); | |
// notify observers | |
this.observers.notify("onDeleteItem", [aItem]); | |
}, | |
// void getItem( in string id, in calIOperationListener aListener ); | |
getItem: function cSC_getItem(aId, aListener) { | |
if (!aListener) | |
return; | |
var item = this.getItemById (aId); | |
if (!item) { | |
// querying by id is a valid use case, even if no item is returned: | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.GET, | |
aId, | |
null); | |
return; | |
} | |
var item_iid = null; | |
if (isEvent(item)) | |
item_iid = Components.interfaces.calIEvent; | |
else if (isToDo(item)) | |
item_iid = Components.interfaces.calITodo; | |
else { | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_ERROR_FAILURE, | |
Components.interfaces.calIOperationListener.GET, | |
aId, | |
"Can't deduce item type based on QI"); | |
return; | |
} | |
aListener.onGetResult (this.superCalendar, | |
Components.results.NS_OK, | |
item_iid, null, | |
1, [item]); | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.GET, | |
aId, | |
null); | |
}, | |
// void getItems( in unsigned long aItemFilter, in unsigned long aCount, | |
// in calIDateTime aRangeStart, in calIDateTime aRangeEnd, | |
// in calIOperationListener aListener ); | |
getItems: function cSC_getItems(aItemFilter, aCount, | |
aRangeStart, aRangeEnd, aListener) { | |
let this_ = this; | |
cal.postPone(function() { | |
this_.getItems_(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener); | |
}); | |
}, | |
getItems_: function cSC_getItems_(aItemFilter, aCount, | |
aRangeStart, aRangeEnd, aListener) | |
{ | |
//var profStartTime = Date.now(); | |
if (!aListener) | |
return; | |
var self = this; | |
var itemsFound = Array(); | |
var startTime = -0x7fffffffffffffff; | |
// endTime needs to be the max value a PRTime can be | |
var endTime = 0x7fffffffffffffff; | |
var count = 0; | |
if (aRangeStart) | |
startTime = aRangeStart.nativeTime; | |
if (aRangeEnd) | |
endTime = aRangeEnd.nativeTime; | |
var wantUnrespondedInvitations = ((aItemFilter & kCalICalendar.ITEM_FILTER_REQUEST_NEEDS_ACTION) != 0); | |
var superCal; | |
try { | |
superCal = this.superCalendar.QueryInterface(Components.interfaces.calISchedulingSupport); | |
} catch (exc) { | |
wantUnrespondedInvitations = false; | |
} | |
function checkUnrespondedInvitation(item) { | |
var att = superCal.getInvitedAttendee(item); | |
return (att && (att.participationStatus == "NEEDS-ACTION")); | |
} | |
var wantEvents = ((aItemFilter & kCalICalendar.ITEM_FILTER_TYPE_EVENT) != 0); | |
var wantTodos = ((aItemFilter & kCalICalendar.ITEM_FILTER_TYPE_TODO) != 0); | |
var asOccurrences = ((aItemFilter & kCalICalendar.ITEM_FILTER_CLASS_OCCURRENCES) != 0); | |
var wantOfflineDeletedItems = ((aItemFilter & kCalICalendar.ITEM_FILTER_OFFLINE_DELETED) != 0); | |
if (!wantEvents && !wantTodos) { | |
// nothing to do | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.GET, | |
null, | |
null); | |
return; | |
} | |
this.assureRecurringItemCaches(); | |
var itemCompletedFilter = ((aItemFilter & kCalICalendar.ITEM_FILTER_COMPLETED_YES) != 0); | |
var itemNotCompletedFilter = ((aItemFilter & kCalICalendar.ITEM_FILTER_COMPLETED_NO) != 0); | |
function checkCompleted(item) { | |
return (item.isCompleted ? itemCompletedFilter : itemNotCompletedFilter); | |
} | |
// sending items to the listener 1 at a time sucks. instead, | |
// queue them up. | |
// if we ever have more than maxQueueSize items outstanding, | |
// call the listener. Calling with null theItems forces | |
// a send and a queue clear. | |
var maxQueueSize = 10; | |
var queuedItems = [ ]; | |
var queuedItemsIID; | |
function queueItems(theItems, theIID) { | |
// if we're about to start sending a different IID, | |
// flush the queue | |
if (theIID && queuedItemsIID != theIID) { | |
if (queuedItemsIID) | |
queueItems(null); | |
queuedItemsIID = theIID; | |
} | |
if (theItems) | |
queuedItems = queuedItems.concat(theItems); | |
if (queuedItems.length != 0 && (!theItems || queuedItems.length > maxQueueSize)) { | |
//var listenerStart = Date.now(); | |
aListener.onGetResult(self.superCalendar, | |
Components.results.NS_OK, | |
queuedItemsIID, null, | |
queuedItems.length, queuedItems); | |
//var listenerEnd = Date.now(); | |
//dump ("++++ listener callback took: " + (listenerEnd - listenerStart) + " ms\n"); | |
queuedItems = [ ]; | |
} | |
} | |
// helper function to handle converting a row to an item, | |
// expanding occurrences, and queue the items for the listener | |
function handleResultItem(item, theIID, optionalFilterFunc) { | |
var expandedItems = []; | |
if (item.recurrenceInfo && asOccurrences) { | |
// If the item is recurring, get all ocurrences that fall in | |
// the range. If the item doesn't fall into the range at all, | |
// this expands to 0 items. | |
expandedItems = item.recurrenceInfo.getOccurrences(aRangeStart, aRangeEnd, 0, {}); | |
if (wantUnrespondedInvitations) { | |
expandedItems = expandedItems.filter(checkUnrespondedInvitation); | |
} | |
} else if ((!wantUnrespondedInvitations || checkUnrespondedInvitation(item)) && | |
checkIfInRange(item, aRangeStart, aRangeEnd)) { | |
// If no occurrences are wanted, check only the parent item. | |
// This will be changed with bug 416975. | |
expandedItems = [ item ]; | |
} | |
function filterOfflineItems(item){ | |
///Check whether item should be returned based on the ItemFilter | |
let retval = true; | |
if(item.hasProperty("OFFLINE_JOURNAL")) | |
retval = (item.getProperty("OFFLINE_JOURNAL") == "D" & wantOfflineDeletedItems) ? true : false; | |
return retval; | |
} | |
if (expandedItems.length && optionalFilterFunc) { | |
expandedItems = expandedItems.filter(optionalFilterFunc); | |
} | |
if(expandedItems.length && filterOfflineItems){ | |
expandedItems = expandedItems.filter(filterOfflineItems); | |
} | |
queueItems (expandedItems, theIID); | |
return expandedItems.length; | |
} | |
// check the count and send end if count is exceeded | |
function checkCount() { | |
if (aCount && count >= aCount) { | |
// flush queue | |
queueItems(null); | |
// send operation complete | |
self.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.GET, | |
null, | |
null); | |
// tell caller we're done | |
return true; | |
} | |
return false; | |
} | |
// First fetch all the events | |
if (wantEvents) { | |
var sp; // stmt params | |
var resultItems = []; | |
// first get non-recurring events that happen to fall within the range | |
// | |
try { | |
this.prepareStatement(this.mSelectNonRecurringEventsByRange); | |
sp = this.mSelectNonRecurringEventsByRange.params; | |
sp.range_start = startTime; | |
sp.range_end = endTime; | |
sp.start_offset = aRangeStart ? aRangeStart.timezoneOffset * USECS_PER_SECOND : 0; | |
sp.end_offset = aRangeEnd ? aRangeEnd.timezoneOffset * USECS_PER_SECOND : 0; | |
while (this.mSelectNonRecurringEventsByRange.step()) { | |
let row = this.mSelectNonRecurringEventsByRange.row; | |
resultItems.push(this.getEventFromRow(row, {})); | |
} | |
} catch (e) { | |
this.logError("Error selecting non recurring events by range!\n", e); | |
} finally { | |
this.mSelectNonRecurringEventsByRange.reset(); | |
} | |
// process the non-recurring events: | |
for each (var evitem in resultItems) { | |
count += handleResultItem(evitem, Components.interfaces.calIEvent); | |
if (checkCount()) { | |
return; | |
} | |
} | |
// process the recurring events from the cache | |
for each (var evitem in this.mRecEventCache) { | |
count += handleResultItem(evitem, Components.interfaces.calIEvent); | |
if (checkCount()) { | |
return; | |
} | |
} | |
} | |
// if todos are wanted, do them next | |
if (wantTodos) { | |
var sp; // stmt params | |
var resultItems = []; | |
// first get non-recurring todos that happen to fall within the range | |
try { | |
this.prepareStatement(this.mSelectNonRecurringTodosByRange); | |
sp = this.mSelectNonRecurringTodosByRange.params; | |
sp.range_start = startTime; | |
sp.range_end = endTime; | |
sp.start_offset = aRangeStart ? aRangeStart.timezoneOffset * USECS_PER_SECOND : 0; | |
sp.end_offset = aRangeEnd ? aRangeEnd.timezoneOffset * USECS_PER_SECOND : 0; | |
while (this.mSelectNonRecurringTodosByRange.step()) { | |
let row = this.mSelectNonRecurringTodosByRange.row; | |
resultItems.push(this.getTodoFromRow(row, {})); | |
} | |
} catch (e) { | |
this.logError("Error selecting non recurring todos by range", e); | |
} finally { | |
this.mSelectNonRecurringTodosByRange.reset(); | |
} | |
// process the non-recurring todos: | |
for each (var todoitem in resultItems) { | |
count += handleResultItem(todoitem, Components.interfaces.calITodo, checkCompleted); | |
if (checkCount()) { | |
return; | |
} | |
} | |
// Note: Reading the code, completed *occurrences* seems to be broken, because | |
// only the parent item has been filtered; I fixed that. | |
// Moreover item.todo_complete etc seems to be a leftover... | |
// process the recurring todos from the cache | |
for each (var todoitem in this.mRecTodoCache) { | |
count += handleResultItem(todoitem, Components.interfaces.calITodo, checkCompleted); | |
if (checkCount()) { | |
return; | |
} | |
} | |
} | |
// flush the queue | |
queueItems(null); | |
// and finish | |
this.notifyOperationComplete(aListener, | |
Components.results.NS_OK, | |
Components.interfaces.calIOperationListener.GET, | |
null, | |
null); | |
//var profEndTime = Date.now(); | |
//dump ("++++ getItems took: " + (profEndTime - profStartTime) + " ms\n"); | |
}, | |
// | |
// database handling | |
// | |
// database initialization | |
// assumes mDB is valid | |
initDB: function cSC_initDB() { | |
ASSERT(this.mDB, "Database has not been opened!", true); | |
this.mSelectEvent = createStatement ( | |
this.mDB, | |
"SELECT * FROM cal_events " + | |
"WHERE id = :id AND cal_id = :cal_id " + | |
" AND recurrence_id IS NULL " + | |
"LIMIT 1" | |
); | |
this.mSelectTodo = createStatement ( | |
this.mDB, | |
"SELECT * FROM cal_todos " + | |
"WHERE id = :id AND cal_id = :cal_id " + | |
" AND recurrence_id IS NULL " + | |
"LIMIT 1" | |
); | |
// The more readable version of the next where-clause is: | |
// WHERE ((event_end > :range_start OR | |
// (event_end = :range_start AND | |
// event_start = :range_start)) | |
// AND event_start < :range_end) | |
// | |
// but that doesn't work with floating start or end times. The logic | |
// is the same though. | |
// For readability, a few helpers: | |
var floatingEventStart = "event_start_tz = 'floating' AND event_start" | |
var nonFloatingEventStart = "event_start_tz != 'floating' AND event_start" | |
var floatingEventEnd = "event_end_tz = 'floating' AND event_end" | |
var nonFloatingEventEnd = "event_end_tz != 'floating' AND event_end" | |
// The query needs to take both floating and non floating into account | |
this.mSelectNonRecurringEventsByRange = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_events " + | |
"WHERE " + | |
" (("+floatingEventEnd+" > :range_start + :start_offset) OR " + | |
" ("+nonFloatingEventEnd+" > :range_start) OR " + | |
" ((("+floatingEventEnd+" = :range_start + :start_offset) OR " + | |
" ("+nonFloatingEventEnd+" = :range_start)) AND " + | |
" (("+floatingEventStart+" = :range_start + :start_offset) OR " + | |
" ("+nonFloatingEventStart+" = :range_start)))) " + | |
" AND " + | |
" (("+floatingEventStart+" < :range_end + :end_offset) OR " + | |
" ("+nonFloatingEventStart+" < :range_end)) " + | |
" AND cal_id = :cal_id AND flags & 16 == 0 AND recurrence_id IS NULL" | |
); | |
/** | |
* WHERE (due > rangeStart AND start < rangeEnd) OR | |
* (due = rangeStart AND start = rangeStart) OR | |
* (due IS NULL AND ((start >= rangeStart AND start < rangeEnd) OR | |
* (start IS NULL AND | |
* (completed > rangeStart OR completed IS NULL))) OR | |
* (start IS NULL AND due >= rangeStart AND due < rangeEnd) | |
*/ | |
var floatingTodoEntry = "todo_entry_tz = 'floating' AND todo_entry"; | |
var nonFloatingTodoEntry = "todo_entry_tz != 'floating' AND todo_entry"; | |
var floatingTodoDue = "todo_due_tz = 'floating' AND todo_due"; | |
var nonFloatingTodoDue = "todo_due_tz != 'floating' AND todo_due"; | |
var floatingCompleted = "todo_completed_tz = 'floating' AND todo_completed"; | |
var nonFloatingCompleted = "todo_completed_tz != 'floating' AND todo_completed"; | |
this.mSelectNonRecurringTodosByRange = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_todos " + | |
"WHERE " + | |
"(((("+floatingTodoDue+" > :range_start + :start_offset) OR " + | |
" ("+nonFloatingTodoDue+" > :range_start)) AND " + | |
" (("+floatingTodoEntry+" < :range_end + :end_offset) OR " + | |
" ("+nonFloatingTodoEntry+" < :range_end))) OR " + | |
" ((("+floatingTodoDue+" = :range_start + :start_offset) OR " + | |
" ("+nonFloatingTodoDue+" = :range_start)) AND " + | |
" (("+floatingTodoEntry+" = :range_start + :start_offset) OR " + | |
" ("+nonFloatingTodoEntry+" = :range_start))) OR " + | |
" ((todo_due IS NULL) AND " + | |
" ((("+floatingTodoEntry+" >= :range_start + :start_offset) OR " + | |
" ("+nonFloatingTodoEntry+" >= :range_start)) AND " + | |
" (("+floatingTodoEntry+" < :range_end + :end_offset) OR " + | |
" ("+nonFloatingTodoEntry+" < :range_end)))) OR " + | |
" ((todo_entry IS NULL) AND " + | |
" ((("+floatingCompleted+" > :range_start + :start_offset) OR " + | |
" ("+nonFloatingCompleted+" > :range_start)) OR " + | |
" (todo_completed IS NULL)))) " + | |
" AND cal_id = :cal_id AND flags & 16 == 0 AND recurrence_id IS NULL" | |
); | |
this.mSelectEventsWithRecurrence = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_events " + | |
" WHERE flags & 16 == 16 " + | |
" AND cal_id = :cal_id AND recurrence_id is NULL" | |
); | |
this.mSelectTodosWithRecurrence = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_todos " + | |
" WHERE flags & 16 == 16 " + | |
" AND cal_id = :cal_id AND recurrence_id IS NULL" | |
); | |
this.mSelectEventExceptions = createStatement ( | |
this.mDB, | |
"SELECT * FROM cal_events " + | |
"WHERE id = :id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NOT NULL" | |
); | |
this.mSelectTodoExceptions = createStatement ( | |
this.mDB, | |
"SELECT * FROM cal_todos " + | |
"WHERE id = :id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NOT NULL" | |
); | |
// For the extra-item data, we used to use mDBTwo, so that | |
// these could be executed while a selectItems was running. | |
// This no longer seems to be needed and actually causes | |
// havoc when transactions are in use. | |
this.mSelectAttendeesForItem = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_attendees " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NULL" | |
); | |
this.mSelectAttendeesForItemWithRecurrenceId = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_attendees " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id = :recurrence_id" + | |
" AND recurrence_id_tz = :recurrence_id_tz" | |
); | |
this.mSelectPropertiesForItem = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_properties" + | |
" WHERE item_id = :item_id" + | |
" AND cal_id = :cal_id" + | |
" AND recurrence_id IS NULL" | |
); | |
this.mSelectPropertiesForItemWithRecurrenceId = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_properties " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id = :recurrence_id" + | |
" AND recurrence_id_tz = :recurrence_id_tz" | |
); | |
this.mSelectRecurrenceForItem = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_recurrence " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" ORDER BY recur_index" | |
); | |
this.mSelectAttachmentsForItem = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_attachments " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NULL" | |
); | |
this.mSelectAttachmentsForItemWithRecurrenceId = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_attachments" + | |
" WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id = :recurrence_id" + | |
" AND recurrence_id_tz = :recurrence_id_tz" | |
); | |
this.mSelectRelationsForItem = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_relations " + | |
"WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NULL" | |
); | |
this.mSelectRelationsForItemWithRecurrenceId = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_relations" + | |
" WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id = :recurrence_id" + | |
" AND recurrence_id_tz = :recurrence_id_tz" | |
); | |
this.mSelectMetaData = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_metadata" | |
+ " WHERE item_id = :item_id AND cal_id = :cal_id"); | |
this.mSelectAllMetaData = createStatement( | |
this.mDB, | |
"SELECT * FROM cal_metadata" | |
+ " WHERE cal_id = :cal_id"); | |
this.mSelectAlarmsForItem = createStatement( | |
this.mDB, | |
"SELECT icalString FROM cal_alarms" | |
+ " WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id IS NULL" | |
); | |
this.mSelectAlarmsForItemWithRecurrenceId = createStatement( | |
this.mDB, | |
"SELECT icalString FROM cal_alarms" + | |
" WHERE item_id = :item_id AND cal_id = :cal_id" + | |
" AND recurrence_id = :recurrence_id" + | |
" AND recurrence_id_tz = :recurrence_id_tz" | |
); | |
// insert statements | |
this.mInsertEvent = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_events " + | |
" (cal_id, id, time_created, last_modified, " + | |
" title, priority, privacy, ical_status, flags, " + | |
" event_start, event_start_tz, event_end, event_end_tz, event_stamp, " + | |
" recurrence_id, recurrence_id_tz, alarm_last_ack) " + | |
"VALUES (:cal_id, :id, :time_created, :last_modified, " + | |
" :title, :priority, :privacy, :ical_status, :flags, " + | |
" :event_start, :event_start_tz, :event_end, :event_end_tz, :event_stamp, " + | |
" :recurrence_id, :recurrence_id_tz, :alarm_last_ack)" | |
); | |
this.mInsertTodo = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_todos " + | |
" (cal_id, id, time_created, last_modified, " + | |
" title, priority, privacy, ical_status, flags, " + | |
" todo_entry, todo_entry_tz, todo_due, todo_due_tz, todo_stamp, " + | |
" todo_completed, todo_completed_tz, todo_complete, " + | |
" recurrence_id, recurrence_id_tz, alarm_last_ack)" + | |
"VALUES (:cal_id, :id, :time_created, :last_modified, " + | |
" :title, :priority, :privacy, :ical_status, :flags, " + | |
" :todo_entry, :todo_entry_tz, :todo_due, :todo_due_tz, :todo_stamp, " + | |
" :todo_completed, :todo_completed_tz, :todo_complete, " + | |
" :recurrence_id, :recurrence_id_tz, :alarm_last_ack)" | |
); | |
this.mInsertProperty = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_properties (cal_id, item_id, recurrence_id, recurrence_id_tz, key, value) " + | |
"VALUES (:cal_id, :item_id, :recurrence_id, :recurrence_id_tz, :key, :value)" | |
); | |
this.mInsertAttendee = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_attendees " + | |
" (cal_id, item_id, recurrence_id, recurrence_id_tz, attendee_id, common_name, rsvp, role, status, type, is_organizer, properties) " + | |
"VALUES (:cal_id, :item_id, :recurrence_id, :recurrence_id_tz, :attendee_id, :common_name, :rsvp, :role, :status, :type, :is_organizer, :properties)" | |
); | |
this.mInsertRecurrence = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_recurrence " + | |
" (cal_id, item_id, recur_index, recur_type, is_negative, dates, count, end_date, interval, second, minute, hour, day, monthday, yearday, weekno, month, setpos) " + | |
"VALUES (:cal_id, :item_id, :recur_index, :recur_type, :is_negative, :dates, :count, :end_date, :interval, :second, :minute, :hour, :day, :monthday, :yearday, :weekno, :month, :setpos)" | |
); | |
this.mInsertAttachment = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_attachments " + | |
" (cal_id, item_id, data, format_type, encoding, recurrence_id, recurrence_id_tz) " + | |
"VALUES (:cal_id, :item_id, :data, :format_type, :encoding, :recurrence_id, :recurrence_id_tz)" | |
); | |
this.mInsertRelation = createStatement ( | |
this.mDB, | |
"INSERT INTO cal_relations " + | |
" (cal_id, item_id, rel_type, rel_id, recurrence_id, recurrence_id_tz) " + | |
"VALUES (:cal_id, :item_id, :rel_type, :rel_id, :recurrence_id, :recurrence_id_tz)" | |
); | |
this.mInsertMetaData = createStatement( | |
this.mDB, | |
"INSERT INTO cal_metadata" | |
+ " (cal_id, item_id, value)" | |
+ " VALUES (:cal_id, :item_id, :value)"); | |
this.mInsertAlarm = createStatement( | |
this.mDB, | |
"INSERT INTO cal_alarms " + | |
" (cal_id, item_id, icalString, recurrence_id, recurrence_id_tz) " + | |
"VALUES (:cal_id, :item_id, :icalString, :recurrence_id, :recurrence_id_tz) " | |
); | |
// delete statements | |
this.mDeleteEvent = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_events WHERE id = :id AND cal_id = :cal_id" | |
); | |
this.mDeleteTodo = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_todos WHERE id = :id AND cal_id = :cal_id" | |
); | |
this.mDeleteAttendees = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_attendees WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteProperties = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_properties WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteRecurrence = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_recurrence WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteAttachments = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_attachments WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteRelations = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_relations WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteMetaData = createStatement( | |
this.mDB, | |
"DELETE FROM cal_metadata WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
this.mDeleteAlarms = createStatement ( | |
this.mDB, | |
"DELETE FROM cal_alarms WHERE item_id = :item_id AND cal_id = :cal_id" | |
); | |
// These are only used when deleting an entire calendar | |
var extrasTables = [ "cal_attendees", "cal_properties", | |
"cal_recurrence", "cal_attachments", | |
"cal_metadata", "cal_relations", | |
"cal_alarms"]; | |
this.mDeleteEventExtras = new Array(); | |
this.mDeleteTodoExtras = new Array(); | |
for (var table in extrasTables) { | |
this.mDeleteEventExtras[table] = createStatement ( | |
this.mDB, | |
"DELETE FROM " + extrasTables[table] + " WHERE item_id IN" + | |
" (SELECT id FROM cal_events WHERE cal_id = :cal_id)" + | |
" AND cal_id = :cal_id" | |
); | |
this.mDeleteTodoExtras[table] = createStatement ( | |
this.mDB, | |
"DELETE FROM " + extrasTables[table] + " WHERE item_id IN" + | |
" (SELECT id FROM cal_todos WHERE cal_id = :cal_id)" + | |
" AND cal_id = :cal_id" | |
); | |
} | |
// Note that you must delete the "extras" _first_ using the above two | |
// statements, before you delete the events themselves. | |
this.mDeleteAllEvents = createStatement ( | |
this.mDB, | |
"DELETE from cal_events WHERE cal_id = :cal_id" | |
); | |
this.mDeleteAllTodos = createStatement ( | |
this.mDB, | |
"DELETE from cal_todos WHERE cal_id = :cal_id" | |
); | |
this.mDeleteAllMetaData = createStatement( | |
this.mDB, | |
"DELETE FROM cal_metadata" + | |
" WHERE cal_id = :cal_id" | |
); | |
}, | |
// | |
// database reading functions | |
// | |
// read in the common ItemBase attributes from aDBRow, and stick | |
// them on item | |
getItemBaseFromRow: function cSC_getItemBaseFromRow(row, flags, item) { | |
item.calendar = this.superCalendar; | |
item.id = row.id; | |
if (row.title) | |
item.title = row.title; | |
if (row.priority) | |
item.priority = row.priority; | |
if (row.privacy) | |
item.privacy = row.privacy; | |
if (row.ical_status) | |
item.status = row.ical_status; | |
if (row.alarm_last_ack) { | |
// alarm acks are always in utc | |
item.alarmLastAck = newDateTime(row.alarm_last_ack, "UTC"); | |
} | |
if (row.recurrence_id) { | |
item.recurrenceId = newDateTime(row.recurrence_id, row.recurrence_id_tz); | |
if ((row.flags & CAL_ITEM_FLAG.RECURRENCE_ID_ALLDAY) != 0) { | |
item.recurrenceId.isDate = true; | |
} | |
} | |
if (flags) | |
flags.value = row.flags; | |
if (row.time_created) { | |
item.setProperty("CREATED", newDateTime(row.time_created, "UTC")); | |
} | |
// This must be done last because the setting of any other property | |
// after this would overwrite it again. | |
if (row.last_modified) { | |
item.setProperty("LAST-MODIFIED", newDateTime(row.last_modified, "UTC")); | |
} | |
}, | |
cacheItem: function cSC_cacheItem(item) { | |
this.mItemCache[item.id] = item; | |
if (item.recurrenceInfo) { | |
if (isEvent(item)) { | |
this.mRecEventCache[item.id] = item; | |
} else { | |
this.mRecTodoCache[item.id] = item; | |
} | |
} | |
}, | |
assureRecurringItemCaches: function cSC_assureRecurringItemCaches() { | |
if (this.mRecItemCacheInited) { | |
return; | |
} | |
// build up recurring event and todo cache, because we need that on every query: | |
// for recurring items, we need to query database-wide.. yuck | |
try { | |
this.prepareStatement(this.mSelectEventsWithRecurrence); | |
let sp = this.mSelectEventsWithRecurrence.params; | |
while (this.mSelectEventsWithRecurrence.step()) { | |
var row = this.mSelectEventsWithRecurrence.row; | |
var item = this.getEventFromRow(row, {}); | |
this.mRecEventCache[item.id] = item; | |
} | |
} catch (e) { | |
this.logError("Error selecting events with recurrence!", e); | |
} finally { | |
this.mSelectEventsWithRecurrence.reset(); | |
} | |
try { | |
this.prepareStatement(this.mSelectTodosWithRecurrence); | |
sp = this.mSelectTodosWithRecurrence.params; | |
while (this.mSelectTodosWithRecurrence.step()) { | |
var row = this.mSelectTodosWithRecurrence.row; | |
var item = this.getTodoFromRow(row, {}); | |
this.mRecTodoCache[item.id] = item; | |
} | |
} catch (e) { | |
this.logError("Error selecting todos with recurrence!", e); | |
} finally { | |
this.mSelectTodosWithRecurrence.reset(); | |
} | |
this.mRecItemCacheInited = true; | |
}, | |
// xxx todo: consider removing flags parameter | |
getEventFromRow: function cSC_getEventFromRow(row, flags, isException) { | |
var item; | |
if (!isException) { // only parent items are cached | |
item = this.mItemCache[row.id]; | |
if (item) { | |
return item; | |
} | |
} | |
item = createEvent(); | |
if (row.event_start) | |
item.startDate = newDateTime(row.event_start, row.event_start_tz); | |
if (row.event_end) | |
item.endDate = newDateTime(row.event_end, row.event_end_tz); | |
if (row.event_stamp) | |
item.setProperty("DTSTAMP", newDateTime(row.event_stamp, "UTC")); | |
if ((row.flags & CAL_ITEM_FLAG.EVENT_ALLDAY) != 0) { | |
item.startDate.isDate = true; | |
item.endDate.isDate = true; | |
} | |
if(row.offline_journal){ | |
item.setProperty("OFFLINE_JOURNAL", row.offline_journal); | |
} | |
// This must be done last to keep the modification time intact. | |
this.getItemBaseFromRow (row, flags, item); | |
this.getAdditionalDataForItem(item, flags.value); | |
if (!isException) { // keep exceptions modifyable to set the parentItem | |
item.makeImmutable(); | |
this.cacheItem(item); | |
} | |
return item; | |
}, | |
getTodoFromRow: function cSC_getTodoFromRow(row, flags, isException) { | |
var item; | |
if (!isException) { // only parent items are cached | |
item = this.mItemCache[row.id]; | |
if (item) { | |
return item; | |
} | |
} | |
item = createTodo(); | |
if (row.todo_entry) | |
item.entryDate = newDateTime(row.todo_entry, row.todo_entry_tz); | |
if (row.todo_due) | |
item.dueDate = newDateTime(row.todo_due, row.todo_due_tz); | |
if (row.todo_stamp) | |
item.setProperty("DTSTAMP", newDateTime(row.todo_stamp, "UTC")); | |
if (row.todo_completed) | |
item.completedDate = newDateTime(row.todo_completed, row.todo_completed_tz); | |
if (row.todo_complete) | |
item.percentComplete = row.todo_complete; | |
// This must be done last to keep the modification time intact. | |
this.getItemBaseFromRow (row, flags, item); | |
this.getAdditionalDataForItem(item, flags.value); | |
if (!isException) { // keep exceptions modifyable to set the parentItem | |
item.makeImmutable(); | |
this.cacheItem(item); | |
} | |
return item; | |
}, | |
// after we get the base item, we need to check if we need to pull in | |
// any extra data from other tables. We do that here. | |
// We used to use mDBTwo for this, so this can be run while a | |
// select is executing but this no longer seems to be required. | |
getAdditionalDataForItem: function cSC_getAdditionalDataForItem(item, flags) { | |
// This is needed to keep the modification time intact. | |
var savedLastModifiedTime = item.lastModifiedTime; | |
if (flags & CAL_ITEM_FLAG.HAS_ATTENDEES) { | |
var selectItem = null; | |
if (item.recurrenceId == null) | |
selectItem = this.mSelectAttendeesForItem; | |
else { | |
selectItem = this.mSelectAttendeesForItemWithRecurrenceId; | |
this.setDateParamHelper(selectItem.params, "recurrence_id", item.recurrenceId); | |
} | |
try { | |
this.prepareStatement(selectItem); | |
selectItem.params.item_id = item.id; | |
while (selectItem.step()) { | |
var attendee = this.getAttendeeFromRow(selectItem.row); | |
if (attendee.isOrganizer) { | |
item.organizer = attendee; | |
} else { | |
item.addAttendee(attendee); | |
} | |
} | |
} catch (e) { | |
this.logError("Error getting attendees for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
selectItem.reset(); | |
} | |
} | |
var row; | |
if (flags & CAL_ITEM_FLAG.HAS_PROPERTIES) { | |
var selectItem = null; | |
if (item.recurrenceId == null) | |
selectItem = this.mSelectPropertiesForItem; | |
else { | |
selectItem = this.mSelectPropertiesForItemWithRecurrenceId; | |
this.setDateParamHelper(selectItem.params, "recurrence_id", item.recurrenceId); | |
} | |
try { | |
this.prepareStatement(selectItem); | |
selectItem.params.item_id = item.id; | |
while (selectItem.step()) { | |
row = selectItem.row; | |
var name = row.key; | |
switch (name) { | |
case "DURATION": | |
// for events DTEND/DUE is enforced by calEvent/calTodo, so suppress DURATION: | |
break; | |
case "CATEGORIES": { | |
var cats = categoriesStringToArray(row.value); | |
item.setCategories(cats.length, cats); | |
break; | |
} | |
default: | |
item.setProperty(name, row.value); | |
break; | |
} | |
} | |
} catch (e) { | |
this.logError("Error getting extra properties for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
selectItem.reset(); | |
} | |
} | |
var i; | |
if (flags & CAL_ITEM_FLAG.HAS_RECURRENCE) { | |
if (item.recurrenceId) | |
throw Components.results.NS_ERROR_UNEXPECTED; | |
var rec = null; | |
try { | |
this.prepareStatement(this.mSelectRecurrenceForItem); | |
this.mSelectRecurrenceForItem.params.item_id = item.id; | |
while (this.mSelectRecurrenceForItem.step()) { | |
row = this.mSelectRecurrenceForItem.row; | |
var ritem = null; | |
if (row.recur_type == null || | |
row.recur_type == "x-dateset") | |
{ | |
ritem = Components.classes["@mozilla.org/calendar/recurrence-date-set;1"] | |
.createInstance(Components.interfaces.calIRecurrenceDateSet); | |
var dates = row.dates.split(","); | |
for (i = 0; i < dates.length; i++) { | |
var date = textToDate(dates[i]); | |
ritem.addDate(date); | |
} | |
} else if (row.recur_type == "x-date") { | |
ritem = Components.classes["@mozilla.org/calendar/recurrence-date;1"] | |
.createInstance(Components.interfaces.calIRecurrenceDate); | |
var d = row.dates; | |
ritem.date = textToDate(d); | |
} else { | |
ritem = cal.createRecurrenceRule(); | |
ritem.type = row.recur_type; | |
if (row.count) { | |
try { | |
ritem.count = row.count; | |
} catch(exc) { | |
} | |
} else { | |
if (row.end_date) | |
ritem.untilDate = newDateTime(row.end_date, "UTC"); | |
else | |
ritem.untilDate = null; | |
} | |
try { | |
ritem.interval = row.interval; | |
} catch(exc) { | |
} | |
var rtypes = ["second", | |
"minute", | |
"hour", | |
"day", | |
"monthday", | |
"yearday", | |
"weekno", | |
"month", | |
"setpos"]; | |
for (i = 0; i < rtypes.length; i++) { | |
var comp = "BY" + rtypes[i].toUpperCase(); | |
if (row[rtypes[i]]) { | |
var rstr = row[rtypes[i]].toString().split(","); | |
var rarray = []; | |
for (var j = 0; j < rstr.length; j++) { | |
rarray[j] = parseInt(rstr[j]); | |
} | |
ritem.setComponent (comp, rarray.length, rarray); | |
} | |
} | |
} | |
if (row.is_negative) | |
ritem.isNegative = true; | |
if (rec == null) { | |
rec = cal.createRecurrenceInfo(item); | |
} | |
rec.appendRecurrenceItem(ritem); | |
} | |
} catch (e) { | |
this.logError("Error getting recurrence for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
this.mSelectRecurrenceForItem.reset(); | |
} | |
if (rec == null) { | |
dump ("XXXX Expected to find recurrence, but got no items!\n"); | |
} | |
item.recurrenceInfo = rec; | |
} | |
if (flags & CAL_ITEM_FLAG.HAS_EXCEPTIONS) { | |
// it's safe that we don't run into this branch again for exceptions | |
// (getAdditionalDataForItem->get[Event|Todo]FromRow->getAdditionalDataForItem): | |
// every excepton has a recurrenceId and isn't flagged as CAL_ITEM_FLAG.HAS_EXCEPTIONS | |
if (item.recurrenceId) | |
throw Components.results.NS_ERROR_UNEXPECTED; | |
var rec = item.recurrenceInfo; | |
if (cal.isEvent(item)) { | |
this.mSelectEventExceptions.params.id = item.id; | |
this.prepareStatement(this.mSelectEventExceptions); | |
try { | |
while (this.mSelectEventExceptions.step()) { | |
var row = this.mSelectEventExceptions.row; | |
var exc = this.getEventFromRow(row, {}, true /*isException*/); | |
rec.modifyException(exc, true); | |
} | |
} catch (e) { | |
this.logError("Error getting exceptions for event '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
this.mSelectEventExceptions.reset(); | |
} | |
} else if (cal.isToDo(item)) { | |
this.mSelectTodoExceptions.params.id = item.id; | |
this.prepareStatement(this.mSelectTodoExceptions); | |
try { | |
while (this.mSelectTodoExceptions.step()) { | |
var row = this.mSelectTodoExceptions.row; | |
var exc = this.getTodoFromRow(row, {}, true /*isException*/); | |
rec.modifyException(exc, true); | |
} | |
} catch (e) { | |
this.logError("Error getting exceptions for task '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
this.mSelectTodoExceptions.reset(); | |
} | |
} else { | |
throw Components.results.NS_ERROR_UNEXPECTED; | |
} | |
} | |
if (flags & CAL_ITEM_FLAG.HAS_ATTACHMENTS) { | |
let selectAttachment = this.mSelectAttachmentsForItem; | |
if (item.recurrenceId != null) { | |
selectAttachment = this.mSelectAttachmentsForItemWithRecurrenceId; | |
this.setDateParamHelper(selectAttachment.params, "recurrence_id", item.recurrenceId); | |
} | |
try { | |
this.prepareStatement(selectAttachment); | |
selectAttachment.params.item_id = item.id; | |
while (selectAttachment.step()) { | |
let row = selectAttachment.row; | |
let attachment = this.getAttachmentFromRow(row); | |
item.addAttachment(attachment); | |
} | |
} catch (e) { | |
this.logError("Error getting attachments for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
selectAttachment.reset(); | |
} | |
} | |
if (flags & CAL_ITEM_FLAG.HAS_RELATIONS) { | |
let selectRelation = this.mSelectRelationsForItem; | |
if (item.recurrenceId != null) { | |
selectRelation = this.mSelectRelationsForItemWithRecurrenceId; | |
this.setDateParamHelper(selectRelation.params, "recurrence_id", item.recurrenceId); | |
} | |
try { | |
this.prepareStatement(selectRelation); | |
selectRelation.params.item_id = item.id; | |
while (selectRelation.step()) { | |
let row = selectRelation.row; | |
let relation = this.getRelationFromRow(row); | |
item.addRelation(relation); | |
} | |
} catch (e) { | |
this.logError("Error getting relations for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
selectRelation.reset(); | |
} | |
} | |
if (flags & CAL_ITEM_FLAG.HAS_ALARMS) { | |
let selectAlarm = this.mSelectAlarmsForItem; | |
if (item.recurrenceId != null) { | |
selectAlarm = this.mSelectAlarmsForItemWithRecurrenceId; | |
this.setDateParamHelper(selectAlarm.params, "recurrence_id", item.recurrenceId); | |
} | |
try { | |
selectAlarm.params.item_id = item.id; | |
this.prepareStatement(selectAlarm); | |
while (selectAlarm.step()) { | |
let row = selectAlarm.row; | |
let alarm = cal.createAlarm(); | |
alarm.icalString = row.icalString; | |
item.addAlarm(alarm); | |
} | |
} catch (e) { | |
this.logError("Error getting alarms for item '" + | |
item.title + "' (" + item.id + ")!", e); | |
} finally { | |
selectAlarm.reset(); | |
} | |
} | |
// Restore the saved modification time | |
item.setProperty("LAST-MODIFIED", savedLastModifiedTime); | |
}, | |
getAttendeeFromRow: function cSC_getAttendeeFromRow(row) { | |
let a = cal.createAttendee(); | |
a.id = row.attendee_id; | |
a.commonName = row.common_name; | |
switch (row.rsvp) { | |
case 0: | |
a.rsvp = "FALSE"; | |
break; | |
case 1: | |
a.rsvp = "TRUE"; | |
break; | |
// default: keep undefined | |
} | |
a.role = row.role; | |
a.participationStatus = row.status; | |
a.userType = row.type; | |
a.isOrganizer = row.is_organizer; | |
let props = row.properties; | |
if (props) { | |
for each (let pair in props.split(",")) { | |
[key, value] = pair.split(":"); | |
a.setProperty(decodeURIComponent(key), decodeURIComponent(value)); | |
} | |
} | |
return a; | |
}, | |
getAttachmentFromRow: function cSC_getAttachmentFromRow(row) { | |
let a = cal.createAttachment(); | |
// TODO we don't support binary data here, libical doesn't either. | |
a.uri = makeURL(row.data); | |
a.formatType = row.format_type; | |
a.encoding = row.encoding; | |
return a; | |
}, | |
getRelationFromRow: function cSC_getRelationFromRow(row) { | |
let r = cal.createRelation(); | |
r.relType = row.rel_type; | |
r.relId = row.rel_id; | |
return r; | |
}, | |
// | |
// get item from db or from cache with given iid | |
// | |
getItemById: function cSC_getItemById(aID) { | |
this.assureRecurringItemCaches(); | |
// cached? | |
var item = this.mItemCache[aID]; | |
if (item) { | |
return item; | |
} | |
// not cached; need to read from the db | |
var flags = {}; | |
try { | |
// try events first | |
this.prepareStatement(this.mSelectEvent); | |
this.mSelectEvent.params.id = aID; | |
if (this.mSelectEvent.step()) { | |
item = this.getEventFromRow(this.mSelectEvent.row, flags); | |
} | |
} catch (e) { | |
this.logError("Error selecting item by id " + aID + "!", e); | |
} finally { | |
this.mSelectEvent.reset(); | |
} | |
// try todo if event fails | |
if (!item) { | |
try { | |
this.prepareStatement(this.mSelectTodo); | |
this.mSelectTodo.params.id = aID; | |
if (this.mSelectTodo.step()) { | |
item = this.getTodoFromRow(this.mSelectTodo.row, flags); | |
} | |
} catch (e) { | |
this.logError("Error selecting item by id " + aID + "!", e); | |
} finally { | |
this.mSelectTodo.reset(); | |
} | |
} | |
return item; | |
}, | |
// | |
// database writing functions | |
// | |
setDateParamHelper: function cSC_setDateParamHelper(params, entryname, cdt) { | |
if (cdt) { | |
params[entryname] = cdt.nativeTime; | |
var tz = cdt.timezone; | |
var ownTz = cal.getTimezoneService().getTimezone(tz.tzid); | |
if (ownTz) { // if we know that TZID, we use it | |
params[entryname + "_tz"] = ownTz.tzid; | |
} else if (!tz.icalComponent) { // timezone component missing | |
params[entryname + "_tz"] = "floating"; | |
} else { // foreign one | |
params[entryname + "_tz"] = tz.icalComponent.serializeToICS(); | |
} | |
} else { | |
params[entryname] = null; | |
params[entryname + "_tz"] = null; | |
} | |
}, | |
flushItem: function cSC_flushItem(item, olditem) { | |
ASSERT(!item.recurrenceId, "no parent item passed!", true); | |
try { | |
this.deleteItemById(olditem ? olditem.id : item.id); | |
this.acquireTransaction(); | |
this.writeItem(item, olditem); | |
} catch (e) { | |
this.releaseTransaction(e); | |
throw e; | |
} | |
this.releaseTransaction(); | |
this.cacheItem(item); | |
}, | |
// | |
// The write* functions execute the database bits | |
// to write the given item type. They're to return | |
// any bits they want or'd into flags, which will be passed | |
// to writeEvent/writeTodo to actually do the writing. | |
// | |
writeItem: function cSC_writeItem(item, olditem) { | |
var flags = 0; | |
flags |= this.writeAttendees(item, olditem); | |
flags |= this.writeRecurrence(item, olditem); | |
flags |= this.writeProperties(item, olditem); | |
flags |= this.writeAttachments(item, olditem); | |
flags |= this.writeRelations(item, olditem); | |
flags |= this.writeAlarms(item, olditem); | |
if (isEvent(item)) | |
this.writeEvent(item, olditem, flags); | |
else if (isToDo(item)) | |
this.writeTodo(item, olditem, flags); | |
else | |
throw Components.results.NS_ERROR_UNEXPECTED; | |
}, | |
writeEvent: function cSC_writeEvent(item, olditem, flags) { | |
try { | |
this.prepareStatement(this.mInsertEvent); | |
let ip = this.mInsertEvent.params; | |
this.setupItemBaseParams(item, olditem, ip); | |
this.setDateParamHelper(ip, "event_start", item.startDate); | |
this.setDateParamHelper(ip, "event_end", item.endDate); | |
let dtstamp = item.stampTime; | |
if (dtstamp) { | |
ip.event_stamp = dtstamp.nativeTime; | |
} | |
if (item.startDate.isDate) { | |
flags |= CAL_ITEM_FLAG.EVENT_ALLDAY; | |
} | |
ip.flags = flags; | |
this.mInsertEvent.execute(); | |
} finally { | |
this.mInsertEvent.reset(); | |
} | |
}, | |
writeTodo: function cSC_writeTodo(item, olditem, flags) { | |
try { | |
this.prepareStatement(this.mInsertTodo); | |
let ip = this.mInsertTodo.params; | |
this.setupItemBaseParams(item, olditem, ip); | |
this.setDateParamHelper(ip, "todo_entry", item.entryDate); | |
this.setDateParamHelper(ip, "todo_due", item.dueDate); | |
let dtstamp = item.stampTime; | |
if (dtstamp) { | |
ip.todo_stamp = dtstamp.nativeTime; | |
} | |
this.setDateParamHelper(ip, "todo_completed", item.getProperty("COMPLETED")); | |
ip.todo_complete = item.getProperty("PERCENT-COMPLETED"); | |
let someDate = (item.entryDate || item.dueDate); | |
if (someDate && someDate.isDate) { | |
flags |= CAL_ITEM_FLAG.EVENT_ALLDAY; | |
} | |
ip.flags = flags; | |
this.mInsertTodo.execute(); | |
} finally { | |
this.mInsertTodo.reset(); | |
} | |
}, | |
setupItemBaseParams: function cSC_setupItemBaseParams(item, olditem, ip) { | |
ip.id = item.id; | |
if (item.recurrenceId) { | |
this.setDateParamHelper(ip, "recurrence_id", item.recurrenceId); | |
} | |
var tmp; | |
if ((tmp = item.getProperty("CREATED"))) | |
ip.time_created = tmp.nativeTime; | |
if ((tmp = item.getProperty("LAST-MODIFIED"))) | |
ip.last_modified = tmp.nativeTime; | |
ip.title = item.getProperty("SUMMARY"); | |
ip.priority = item.getProperty("PRIORITY"); | |
ip.privacy = item.getProperty("CLASS"); | |
ip.ical_status = item.getProperty("STATUS"); | |
if (item.alarmLastAck) { | |
ip.alarm_last_ack = item.alarmLastAck.nativeTime; | |
} | |
}, | |
writeAttendees: function cSC_writeAttendees(item, olditem) { | |
var attendees = item.getAttendees({}); | |
if (item.organizer) { | |
attendees = attendees.concat([]); | |
attendees.push(item.organizer); | |
} | |
if (attendees.length > 0) { | |
for each (var att in attendees) { | |
var ap = this.mInsertAttendee.params; | |
ap.item_id = item.id; | |
try { | |
this.prepareStatement(this.mInsertAttendee); | |
this.setDateParamHelper(ap, "recurrence_id", item.recurrenceId); | |
ap.attendee_id = att.id; | |
ap.common_name = att.commonName; | |
switch (att.rsvp) { | |
case "FALSE": | |
ap.rsvp = 0; | |
break; | |
case "TRUE": | |
ap.rsvp = 1; | |
break; | |
default: | |
ap.rsvp = 2; | |
break; | |
} | |
ap.role = att.role; | |
ap.status = att.participationStatus; | |
ap.type = att.userType; | |
ap.is_organizer = att.isOrganizer; | |
var props = ""; | |
var propEnum = att.propertyEnumerator; | |
while (propEnum && propEnum.hasMoreElements()) { | |
var prop = propEnum.getNext().QueryInterface(Components.interfaces.nsIProperty); | |
if (props.length) { | |
props += ","; | |
} | |
props += encodeURIComponent(prop.name); | |
props += ":"; | |
props += encodeURIComponent(prop.value); | |
} | |
if (props.length) { | |
ap.properties = props; | |
} | |
this.mInsertAttendee.execute(); | |
} finally { | |
this.mInsertAttendee.reset(); | |
} | |
} | |
return CAL_ITEM_FLAG.HAS_ATTENDEES; | |
} | |
return 0; | |
}, | |
writeProperty: function cSC_writeProperty(item, propName, propValue) { | |
try { | |
this.prepareStatement(this.mInsertProperty); | |
var pp = this.mInsertProperty.params; | |
pp.key = propName; | |
if (calInstanceOf(propValue, Components.interfaces.calIDateTime)) { | |
pp.value = propValue.nativeTime; | |
} else { | |
try { | |
pp.value = propValue; | |
} catch (e) { | |
// The storage service throws an NS_ERROR_ILLEGAL_VALUE in | |
// case pval is something complex (i.e not a string or | |
// number). Swallow this error, leaving the value empty. | |
if (e.result != Components.results.NS_ERROR_ILLEGAL_VALUE) { | |
throw e; | |
} | |
} | |
} | |
pp.item_id = item.id; | |
this.setDateParamHelper(pp, "recurrence_id", item.recurrenceId); | |
this.mInsertProperty.execute(); | |
} finally { | |
this.mInsertProperty.reset(); | |
} | |
}, | |
writeProperties: function cSC_writeProperties(item, olditem) { | |
var ret = 0; | |
var propEnumerator = item.propertyEnumerator; | |
while (propEnumerator.hasMoreElements()) { | |
ret = CAL_ITEM_FLAG.HAS_PROPERTIES; | |
var prop = propEnumerator.getNext().QueryInterface(Components.interfaces.nsIProperty); | |
if (item.isPropertyPromoted(prop.name)) | |
continue; | |
this.writeProperty(item, prop.name, prop.value); | |
} | |
var cats = item.getCategories({}); | |
if (cats.length > 0) { | |
ret = CAL_ITEM_FLAG.HAS_PROPERTIES; | |
this.writeProperty(item, "CATEGORIES", categoriesArrayToString(cats)); | |
} | |
return ret; | |
}, | |
writeRecurrence: function cSC_writeRecurrence(item, olditem) { | |
var flags = 0; | |
var rec = item.recurrenceInfo; | |
if (rec) { | |
flags = CAL_ITEM_FLAG.HAS_RECURRENCE; | |
var ritems = rec.getRecurrenceItems ({}); | |
for (i in ritems) { | |
var ritem = ritems[i]; | |
var ap = this.mInsertRecurrence.params; | |
try { | |
this.prepareStatement(this.mInsertRecurrence); | |
ap.item_id = item.id; | |
ap.recur_index = i; | |
ap.is_negative = ritem.isNegative; | |
if (calInstanceOf(ritem, Components.interfaces.calIRecurrenceDate)) { | |
ap.recur_type = "x-date"; | |
ap.dates = dateToText(getInUtcOrKeepFloating(ritem.date)); | |
} else if (calInstanceOf(ritem, Components.interfaces.calIRecurrenceDateSet)) { | |
ap.recur_type = "x-dateset"; | |
var rdates = ritem.getDates({}); | |
var datestr = ""; | |
for (j in rdates) { | |
if (j != 0) | |
datestr += ","; | |
datestr += dateToText(getInUtcOrKeepFloating(rdates[j])); | |
} | |
ap.dates = datestr; | |
} else if (calInstanceOf(ritem, Components.interfaces.calIRecurrenceRule)) { | |
ap.recur_type = ritem.type; | |
if (ritem.isByCount) | |
ap.count = ritem.count; | |
else | |
ap.end_date = ritem.untilDate ? ritem.untilDate.nativeTime : null; | |
ap.interval = ritem.interval; | |
var rtypes = ["second", | |
"minute", | |
"hour", | |
"day", | |
"monthday", | |
"yearday", | |
"weekno", | |
"month", | |
"setpos"]; | |
for (var j = 0; j < rtypes.length; j++) { | |
var comp = "BY" + rtypes[j].toUpperCase(); | |
var comps = ritem.getComponent(comp, {}); | |
if (comps && comps.length > 0) { | |
var compstr = comps.join(","); | |
ap[rtypes[j]] = compstr; | |
} | |
} | |
} else { | |
dump ("##### Don't know how to serialize recurrence item " + ritem + "!\n"); | |
} | |
this.mInsertRecurrence.execute(); | |
} finally { | |
this.mInsertRecurrence.reset(); | |
} | |
} | |
var exceptions = rec.getExceptionIds ({}); | |
if (exceptions.length > 0) { | |
flags |= CAL_ITEM_FLAG.HAS_EXCEPTIONS; | |
// we need to serialize each exid as a separate | |
// event/todo; setupItemBase will handle | |
// writing the recurrenceId for us | |
for each (exid in exceptions) { | |
let ex = rec.getExceptionFor(exid); | |
if (!ex) | |
throw Components.results.NS_ERROR_UNEXPECTED; | |
this.writeItem(ex, null); | |
} | |
} | |
} else if (item.recurrenceId && item.recurrenceId.isDate) { | |
flags |= CAL_ITEM_FLAG.RECURRENCE_ID_ALLDAY; | |
} | |
return flags; | |
}, | |
writeAttachments: function cSC_writeAttachments(item, olditem) { | |
let attachments = item.getAttachments({}); | |
if (attachments && attachments.length > 0) { | |
for each (att in attachments) { | |
let ap = this.mInsertAttachment.params; | |
try { | |
this.prepareStatement(this.mInsertAttachment); | |
this.setDateParamHelper(ap, "recurrence_id", item.recurrenceId); | |
ap.item_id = item.id; | |
ap.data = (att.uri ? att.uri.spec : ""); | |
ap.format_type = att.formatType; | |
ap.encoding = att.encoding; | |
this.mInsertAttachment.execute(); | |
} finally { | |
this.mInsertAttachment.reset(); | |
} | |
} | |
return CAL_ITEM_FLAG.HAS_ATTACHMENTS; | |
} | |
return 0; | |
}, | |
writeRelations: function cSC_writeRelations(item, olditem) { | |
let relations = item.getRelations({}); | |
if (relations && relations.length > 0) { | |
for each (var rel in relations) { | |
let rp = this.mInsertRelation.params; | |
try { | |
this.prepareStatement(this.mInsertRelation); | |
this.setDateParamHelper(rp, "recurrence_id", item.recurrenceId); | |
rp.item_id = item.id; | |
rp.rel_type = rel.relType; | |
rp.rel_id = rel.relId; | |
this.mInsertRelation.execute(); | |
} finally { | |
this.mInsertRelation.reset(); | |
} | |
} | |
return CAL_ITEM_FLAG.HAS_RELATIONS; | |
} | |
return 0; | |
}, | |
writeAlarms: function cSC_writeAlarms(item, olditem) { | |
let alarms = item.getAlarms({}); | |
if (alarms.length < 1) { | |
return 0; | |
} | |
for each (let alarm in alarms) { | |
let pp = this.mInsertAlarm.params; | |
try { | |
this.prepareStatement(this.mInsertAlarm); | |
this.setDateParamHelper(pp, "recurrence_id", item.recurrenceId); | |
pp.item_id = item.id; | |
pp.icalString = alarm.icalString; | |
this.mInsertAlarm.execute(); | |
} catch(e) { | |
this.logError("Error writing alarm for item " + item.title + " (" + item.id + ")", e); | |
} finally { | |
this.mInsertAlarm.reset(); | |
} | |
} | |
return CAL_ITEM_FLAG.HAS_ALARMS; | |
}, | |
/** | |
* Deletes the item with the given item id. | |
* | |
* @param aID The id of the item to delete. | |
*/ | |
deleteItemById: function cSC_deleteItemById(aID) { | |
this.acquireTransaction(); | |
try { | |
this.mDeleteAttendees(aID, this.id); | |
this.mDeleteProperties(aID, this.id); | |
this.mDeleteRecurrence(aID, this.id); | |
this.mDeleteEvent(aID, this.id); | |
this.mDeleteTodo(aID, this.id); | |
this.mDeleteAttachments(aID, this.id); | |
this.mDeleteRelations(aID, this.id); | |
this.mDeleteMetaData(aID, this.id); | |
this.mDeleteAlarms(aID, this.id); | |
} catch (e) { | |
this.releaseTransaction(e); | |
throw e; | |
} | |
this.releaseTransaction(); | |
delete this.mItemCache[aID]; | |
delete this.mRecEventCache[aID]; | |
delete this.mRecTodoCache[aID]; | |
}, | |
/** | |
* Acquire a transaction for this calendar. | |
*/ | |
acquireTransaction: function cSC_acquireTransaction() { | |
this.mDB.beginTransaction(); | |
}, | |
/** | |
* Releases one level of transactions for this calendar. | |
* | |
* @param err (optional) If set, the transaction is set to fail when | |
* the count reaches zero. | |
*/ | |
releaseTransaction: function cSC_releaseTransaction(err) { | |
if (err) { | |
cal.ERROR("DB error: " + this.mDB.lastErrorString + "\nexc: " + err); | |
this.mDB.rollbackTransaction(); | |
} else { | |
this.mDB.commitTransaction(); | |
} | |
}, | |
// | |
// calISyncWriteCalendar interface | |
// | |
setMetaData: function cSC_setMetaData(id, value) { | |
this.mDeleteMetaData(id, this.id); | |
try { | |
this.prepareStatement(this.mInsertMetaData); | |
var sp = this.mInsertMetaData.params; | |
sp.item_id = id; | |
sp.value = value; | |
this.mInsertMetaData.execute(); | |
} catch (e) { | |
// The storage service throws an NS_ERROR_ILLEGAL_VALUE in | |
// case pval is something complex (i.e not a string or | |
// number). Swallow this error, leaving the value empty. | |
if (e.result != Components.results.NS_ERROR_ILLEGAL_VALUE) { | |
this.logError("Error setting metadata for id " + id + "!", e); | |
throw e; | |
} | |
} finally { | |
this.mInsertMetaData.reset(); | |
} | |
}, | |
deleteMetaData: function cSC_deleteMetaData(id) { | |
this.mDeleteMetaData(id, this.id); | |
}, | |
getMetaData: function cSC_getMetaData(id) { | |
let query = this.mSelectMetaData; | |
try { | |
this.prepareStatement(query); | |
query.params.item_id = id; | |
let value = null; | |
if (query.step()) { | |
value = query.row.value; | |
} | |
} catch (e) { | |
this.logError("Error getting metadata for id " + id + "!", e); | |
} finally { | |
query.reset(); | |
} | |
return value; | |
}, | |
getAllMetaData: function cSC_getAllMetaData(out_count, | |
out_ids, | |
out_values) { | |
let query = this.mSelectAllMetaData; | |
try { | |
this.prepareStatement(query); | |
let ids = []; | |
let values = []; | |
while (query.step()) { | |
ids.push(query.row.item_id); | |
values.push(query.row.value); | |
} | |
out_count.value = ids.length; | |
out_ids.value = ids; | |
out_values.value = values; | |
} catch (e) { | |
this.logError("Error getting all metadata!", e); | |
} finally { | |
query.reset(); | |
} | |
}, | |
/** | |
* Internal logging function that should be called on any database error, | |
* it will log as much info as possible about the database context and | |
* last statement so the problem can be investigated more easilly. | |
* | |
* @param message Error message to log. | |
* @param exception Exception that caused the error. | |
*/ | |
logError: function cSC_logError(message,exception) { | |
let logMessage = "Message: " + message; | |
if (this.mDB) { | |
if (this.mDB.connectionReady) { | |
logMessage += "\nConnection Ready: " + this.mDB.connectionReady; | |
} | |
if (this.mDB.lastError) { | |
logMessage += "\nLast DB Error Number: " + this.mDB.lastError; | |
} | |
if (this.mDB.lastErrorString) { | |
logMessage += "\nLast DB Error Message: " + this.mDB.lastErrorString; | |
} | |
if (this.mDB.databaseFile) { | |
logMessage += "\nDatabase File: " + this.mDB.databaseFile.path; | |
} | |
if (this.mDB.lastInsertRowId) { | |
logMessage += "\nLast Insert Row Id: " + this.mDB.lastInsertRowId; | |
} | |
if (this.mDB.transactionInProgress) { | |
logMessage += "\nTransaction In Progress: " + this.mDB.transactionInProgress; | |
} | |
} | |
if (this.mLastStatement) { | |
logMessage += "\nLast DB Statement: " + this.mLastStatement; | |
if (this.mLastStatement.params) { | |
for (let param in this.mLastStatement.params) { | |
logMessage += "\nLast Statement param [" + param + "]: " + this.mLastStatement.params[param]; | |
} | |
} | |
} | |
if (exception) { | |
logMessage += "\nException: " + exception; | |
} | |
cal.ERROR(logMessage + "\n" + STACK(10)); | |
} | |
}; | |
/** Module Registration */ | |
const scriptLoadOrder = [ | |
"calUtils.js", | |
]; | |
function NSGetFactory(cid) { | |
if (!this.scriptsLoaded) { | |
Services.io.getProtocolHandler("resource") | |
.QueryInterface(Components.interfaces.nsIResProtocolHandler) | |
.setSubstitution("calendar", Services.io.newFileURI(__LOCATION__.parent.parent)); | |
Components.utils.import("resource://calendar/modules/calUtils.jsm"); | |
cal.loadScripts(scriptLoadOrder, Components.utils.getGlobalForObject(this)); | |
this.scriptsLoaded = true; | |
} | |
return (XPCOMUtils.generateNSGetFactory([calStorageCalendar]))(cid); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment