Skip to content

Instantly share code, notes, and snippets.

@creativepsyco
Created June 8, 2011 13:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save creativepsyco/1014460 to your computer and use it in GitHub Desktop.
Save creativepsyco/1014460 to your computer and use it in GitHub Desktop.
calcachedCalendar [2nd Day]
/* ***** 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 <stuart.parmenter@oracle.com>
* Matthew Willis <lilmatt@mozilla.com>
* Michiel van Leeuwen <mvl@exedo.nl>
* Martin Schroeder <mschroeder@mozilla.x-home.org>
* Philipp Kewisch <mozilla@kewis.ch>
* Daniel Boelzle <daniel.boelzle@sun.com>
*
* 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);
}
}
};
/* ***** 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 <mozilla@kewis.ch>
* Daniel Boelzle <daniel.boelzle@sun.com>
* 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"], [], []);
})();
/* ***** 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 <vladimir.vukicevic@oracle.com>
* Joey Minta <jminta@gmail.com>
* Dan Mosedale <dan.mosedale@oracle.com>
* Thomas Benisch <thomas.benisch@sun.com>
* Matthew Willis <lilmatt@mozilla.com>
* Philipp Kewisch <mozilla@kewis.ch>
* Daniel Boelzle <daniel.boelzle@sun.com>
* Sebastian Schwieger <sebo.moz@googlemail.com>
* Fred Jendrzejewski <fred.jen@web.de>
*
* 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