summaryrefslogtreecommitdiffstats
path: root/content/modules/db.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/modules/db.js460
1 files changed, 460 insertions, 0 deletions
diff --git a/content/modules/db.js b/content/modules/db.js
new file mode 100644
index 0000000..730f19a
--- /dev/null
+++ b/content/modules/db.js
@@ -0,0 +1,460 @@
+/*
+ * This file is part of TbSync.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+ "use strict";
+
+var { DeferredTask } = ChromeUtils.import("resource://gre/modules/DeferredTask.jsm");
+
+var db = {
+
+ loaded: false,
+
+ files: {
+ accounts: {
+ name: "accounts68.json",
+ default: JSON.stringify({ sequence: 0, data : {} })
+ //data[account] = {row}
+ },
+ folders: {
+ name: "folders68.json",
+ default: JSON.stringify({})
+ //assoziative array of assoziative array : folders[<int>accountID][<string>folderID] = {row}
+ },
+ changelog: {
+ name: "changelog68.json",
+ default: JSON.stringify([]),
+ },
+ },
+
+ load: async function () {
+ //DB Concept:
+ //-- on application start, data is read async from json file into object
+ //-- add-on only works on object
+ //-- each time data is changed, an async write job is initiated <writeDelay>ms in the future and is resceduled, if another request arrives within that time
+
+ for (let f in this.files) {
+ this.files[f].write = new DeferredTask(() => this.writeAsync(f), 6000);
+
+ try {
+ this[f] = await IOUtils.readJSON(TbSync.io.getAbsolutePath(this.files[f].name));
+ this.files[f].found = true;
+ } catch (e) {
+ //if there is no file, there is no file...
+ this[f] = JSON.parse(this.files[f].default);
+ this.files[f].found = false;
+ Components.utils.reportError(e);
+ }
+ }
+
+ function getNewDeviceId4Migration() {
+ //taken from https://jsfiddle.net/briguy37/2MVFd/
+ let d = new Date().getTime();
+ let uuid = 'xxxxxxxxxxxxxxxxyxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ let r = (d + Math.random()*16)%16 | 0;
+ d = Math.floor(d/16);
+ return (c=='x' ? r : (r&0x3|0x8)).toString(16);
+ });
+ return "MZTB" + uuid;
+ }
+
+ // try to migrate old accounts file from TB60
+ if (!this.files["accounts"].found) {
+ try {
+ let accounts = await IOUtils.readJSON(TbSync.io.getAbsolutePath("accounts.json"));
+ for (let d of Object.values(accounts.data)) {
+ console.log("Migrating: " + JSON.stringify(d));
+
+ let settings = {};
+ settings.status = "disabled";
+ settings.provider = d.provider;
+ settings.https = (d.https == "1");
+
+ switch (d.provider) {
+ case "dav":
+ settings.calDavHost = d.host ? d.host : "";
+ settings.cardDavHost = d.host2 ? d.host2 : "";
+ settings.serviceprovider = d.serviceprovider;
+ settings.user = d.user;
+ settings.syncGroups = (d.syncGroups == "1");
+ settings.useCalendarCache = (d.useCache == "1");
+ break;
+
+ case "eas":
+ settings.useragent = d.useragent;
+ settings.devicetype = d.devicetype;
+ settings.deviceId = getNewDeviceId4Migration();
+ settings.asversionselected = d.asversionselected;
+ settings.asversion = d.asversion;
+ settings.host = d.host;
+ settings.user = d.user;
+ settings.servertype = d.servertype;
+ settings.seperator = d.seperator;
+ settings.provision = (d.provision == "1");
+ settings.displayoverride = (d.displayoverride == "1");
+ if (d.hasOwnProperty("galautocomplete")) settings.galautocomplete = (d.galautocomplete == "1");
+ break;
+ }
+
+ this.addAccount(d.accountname, settings);
+ }
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ this.loaded = true;
+ },
+
+ unload: async function () {
+ if (this.loaded) {
+ for (let f in this.files) {
+ try{
+ //abort write delay timers and write current file content to disk
+ await this.files[f].write.finalize();
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+ }
+ },
+
+
+ saveFile: function (f) {
+ if (this.loaded) {
+ //cancel any pending write and schedule a new delayed write
+ this.files[f].write.disarm();
+ this.files[f].write.arm();
+ }
+ },
+
+ writeAsync: async function (f) {
+ // if this file was not found/read on load, do not write default content to prevent clearing of data in case of read-errors
+ if (!this.files[f].found && JSON.stringify(this[f]) == this.files[f].default) {
+ return;
+ }
+
+ let filepath = TbSync.io.getAbsolutePath(this.files[f].name);
+ await IOUtils.writeJSON(filepath, this[f]);
+ },
+
+
+
+ // simple convenience wrapper
+ saveAccounts: function () {
+ this.saveFile("accounts");
+ },
+
+ saveFolders: function () {
+ this.saveFile("folders");
+ },
+
+ saveChangelog: function () {
+ this.saveFile("changelog");
+ },
+
+
+
+ // CHANGELOG FUNCTIONS
+ getItemStatusFromChangeLog: function (parentId, itemId) {
+ for (let i=0; i<this.changelog.length; i++) {
+ if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) return this.changelog[i].status;
+ }
+ return null;
+ },
+
+ getItemDataFromChangeLog: function (parentId, itemId) {
+ for (let i=0; i<this.changelog.length; i++) {
+ if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) return this.changelog[i];
+ }
+ return null;
+ },
+
+ addItemToChangeLog: function (parentId, itemId, status) {
+ this.removeItemFromChangeLog(parentId, itemId);
+
+ //ChangelogData object
+ let row = {
+ "parentId" : parentId,
+ "itemId" : itemId,
+ "timestamp": Date.now(),
+ "status" : status};
+
+ this.changelog.push(row);
+ this.saveChangelog();
+ },
+
+ removeItemFromChangeLog: function (parentId, itemId, moveToEnd = false) {
+ for (let i=this.changelog.length-1; i>-1; i-- ) {
+ if (this.changelog[i].parentId == parentId && this.changelog[i].itemId == itemId) {
+ let row = this.changelog.splice(i,1);
+ if (moveToEnd) this.changelog.push(row[0]);
+ this.saveChangelog();
+ return;
+ }
+ }
+ },
+
+ removeAllItemsFromChangeLogWithStatus: function (parentId, status) {
+ for (let i=this.changelog.length-1; i>-1; i-- ) {
+ if (this.changelog[i].parentId == parentId && this.changelog[i].status == status) {
+ let row = this.changelog.splice(i,1);
+ }
+ }
+ this.saveChangelog();
+ },
+
+ // Remove all cards of a parentId from ChangeLog
+ clearChangeLog: function (parentId) {
+ if (parentId) {
+ // we allow extra parameters added to a parentId, but still want to delete all items of that parent
+ // so we check for startsWith instead of equal
+ for (let i=this.changelog.length-1; i>-1; i-- ) {
+ if (this.changelog[i].parentId.startsWith(parentId)) this.changelog.splice(i,1);
+ }
+ this.saveChangelog();
+ }
+ },
+
+ getItemsFromChangeLog: function (parentId, maxnumbertosend, status = null) {
+ //maxnumbertosend = 0 will return all results
+ let log = [];
+ let counts = 0;
+ for (let i=0; i<this.changelog.length && (log.length < maxnumbertosend || maxnumbertosend == 0); i++) {
+ if (this.changelog[i].parentId == parentId && (status === null || (typeof this.changelog[i].status == "string" && this.changelog[i].status.indexOf(status) != -1))) log.push(this.changelog[i]);
+ }
+ return log;
+ },
+
+
+
+
+
+ // ACCOUNT FUNCTIONS
+
+ addAccount: function (accountname, newAccountEntry) {
+ this.accounts.sequence++;
+ let id = this.accounts.sequence.toString();
+ newAccountEntry.accountID = id;
+ newAccountEntry.accountname = accountname;
+
+ this.accounts.data[id] = newAccountEntry;
+ this.saveAccounts();
+ return id;
+ },
+
+ removeAccount: function (accountID) {
+ //check if accountID is known
+ if (this.accounts.data.hasOwnProperty(accountID) == false ) {
+ throw "Unknown accountID!" + "\nThrown by db.removeAccount("+accountID+ ")";
+ } else {
+ delete (this.accounts.data[accountID]);
+ delete (this.folders[accountID]);
+ this.saveAccounts();
+ this.saveFolders();
+ }
+ },
+
+ getAccounts: function () {
+ let accounts = {};
+ accounts.IDs = Object.keys(this.accounts.data).filter(accountID => TbSync.providers.loadedProviders.hasOwnProperty(this.accounts.data[accountID].provider)).sort((a, b) => a - b);
+ accounts.allIDs = Object.keys(this.accounts.data).sort((a, b) => a - b)
+ accounts.data = this.accounts.data;
+ return accounts;
+ },
+
+ getAccount: function (accountID) {
+ //check if accountID is known
+ if (this.accounts.data.hasOwnProperty(accountID) == false ) {
+ throw "Unknown accountID!" + "\nThrown by db.getAccount("+accountID+ ")";
+ } else {
+ return this.accounts.data[accountID];
+ }
+ },
+
+ isValidAccountProperty: function (provider, name) {
+ if (["provider"].includes(name)) //internal properties, do not need to be defined by user/provider
+ return true;
+
+ //check if provider is installed
+ if (!TbSync.providers.loadedProviders.hasOwnProperty(provider)) {
+ TbSync.dump("Error @ isValidAccountProperty", "Unknown provider <"+provider+">!");
+ return false;
+ }
+
+ if (TbSync.providers.getDefaultAccountEntries(provider).hasOwnProperty(name)) {
+ return true;
+ } else {
+ TbSync.dump("Error @ isValidAccountProperty", "Unknown account setting <"+name+">!");
+ return false;
+ }
+ },
+
+ getAccountProperty: function (accountID, name) {
+ // if the requested accountID does not exist, getAccount() will fail
+ let data = this.getAccount(accountID);
+
+ //check if field is allowed and get value or default value if setting is not set
+ if (this.isValidAccountProperty(data.provider, name)) {
+ if (data.hasOwnProperty(name)) return data[name];
+ else return TbSync.providers.getDefaultAccountEntries(data.provider)[name];
+ }
+ },
+
+ setAccountProperty: function (accountID , name, value) {
+ // if the requested accountID does not exist, getAccount() will fail
+ let data = this.getAccount(accountID);
+
+ //check if field is allowed, and set given value
+ if (this.isValidAccountProperty(data.provider, name)) {
+ this.accounts.data[accountID][name] = value;
+ }
+ this.saveAccounts();
+ },
+
+ resetAccountProperty: function (accountID , name) {
+ // if the requested accountID does not exist, getAccount() will fail
+ let data = this.getAccount(accountID);
+ let defaults = TbSync.providers.getDefaultAccountEntries(data.provider);
+
+ //check if field is allowed, and set given value
+ if (this.isValidAccountProperty(data.provider, name)) {
+ this.accounts.data[accountID][name] = defaults[name];
+ }
+ this.saveAccounts();
+ },
+
+
+
+
+ // FOLDER FUNCTIONS
+
+ addFolder: function(accountID) {
+ let folderID = TbSync.generateUUID();
+ let provider = this.getAccountProperty(accountID, "provider");
+
+ if (!this.folders.hasOwnProperty(accountID)) this.folders[accountID] = {};
+
+ //create folder with default settings
+ this.folders[accountID][folderID] = TbSync.providers.getDefaultFolderEntries(accountID);
+ this.saveFolders();
+ return folderID;
+ },
+
+ deleteFolder: function(accountID, folderID) {
+ delete (this.folders[accountID][folderID]);
+ //if there are no more folders, delete entire account entry
+ if (Object.keys(this.folders[accountID]).length === 0) delete (this.folders[accountID]);
+ this.saveFolders();
+ },
+
+ isValidFolderProperty: function (accountID, field) {
+ if (["cached"].includes(field)) //internal properties, do not need to be defined by user/provider
+ return true;
+
+ //check if provider is installed
+ let provider = this.getAccountProperty(accountID, "provider");
+ if (!TbSync.providers.loadedProviders.hasOwnProperty(provider)) {
+ TbSync.dump("Error @ isValidFolderProperty", "Unknown provider <"+provider+"> for accountID <"+accountID+">!");
+ return false;
+ }
+
+ if (TbSync.providers.getDefaultFolderEntries(accountID).hasOwnProperty(field)) {
+ return true;
+ } else {
+ TbSync.dump("Error @ isValidFolderProperty", "Unknown folder setting <"+field+"> for accountID <"+accountID+">!");
+ return false;
+ }
+ },
+
+ getFolderProperty: function(accountID, folderID, field) {
+ //does the field exist?
+ let folder = (this.folders.hasOwnProperty(accountID) && this.folders[accountID].hasOwnProperty(folderID)) ? this.folders[accountID][folderID] : null;
+
+ if (folder === null) {
+ throw "Unknown folder <"+folderID+">!";
+ }
+
+ if (this.isValidFolderProperty(accountID, field)) {
+ if (folder.hasOwnProperty(field)) {
+ return folder[field];
+ } else {
+ let provider = this.getAccountProperty(accountID, "provider");
+ let defaultFolder = TbSync.providers.getDefaultFolderEntries(accountID);
+ //handle internal fields, that do not have a default value (see isValidFolderProperty)
+ return (defaultFolder[field] ? defaultFolder[field] : "");
+ }
+ }
+ },
+
+ setFolderProperty: function (accountID, folderID, field, value) {
+ if (this.isValidFolderProperty(accountID, field)) {
+ this.folders[accountID][folderID][field] = value;
+ this.saveFolders();
+ }
+ },
+
+ resetFolderProperty: function (accountID, folderID, field) {
+ let provider = this.getAccountProperty(accountID, "provider");
+ let defaults = TbSync.providers.getDefaultFolderEntries(accountID);
+ if (this.isValidFolderProperty(accountID, field)) {
+ //handle internal fields, that do not have a default value (see isValidFolderProperty)
+ this.folders[accountID][folderID][field] = defaults[field] ? defaults[field] : "";
+ this.saveFolders();
+ }
+ },
+
+ findFolders: function (folderQuery = {}, accountQuery = {}) {
+ // folderQuery is an object with one or more key:value pairs (logical AND) ::
+ // {key1: value1, key2: value2}
+ // the value itself may be an array (logical OR)
+ let data = [];
+ let folderQueryEntries = Object.entries(folderQuery);
+ let folderFields = folderQueryEntries.map(pair => pair[0]);
+ let folderValues = folderQueryEntries.map(pair => Array.isArray(pair[1]) ? pair[1] : [pair[1]]);
+
+ let accountQueryEntries = Object.entries(accountQuery);
+ let accountFields = accountQueryEntries.map(pair => pair[0]);
+ let accountValues = accountQueryEntries.map(pair => Array.isArray(pair[1]) ? pair[1] : [pair[1]]);
+
+ for (let aID in this.folders) {
+ //is this a leftover folder of an account, which no longer there?
+ if (!this.accounts.data.hasOwnProperty(aID)) {
+ delete (this.folders[aID]);
+ this.saveFolders();
+ continue;
+ }
+
+ //skip this folder, if it belongs to an account currently not supported (provider not loaded)
+ if (!TbSync.providers.loadedProviders.hasOwnProperty(this.getAccountProperty(aID, "provider"))) {
+ continue;
+ }
+
+ //does this account match account search options?
+ let accountmatch = true;
+ for (let a = 0; a < accountFields.length && accountmatch; a++) {
+ accountmatch = accountValues[a].some(item => item === this.getAccountProperty(aID, accountFields[a]));
+ //Services.console.logStringMessage(" " + accountFields[a] + ":" + this.getAccountProperty(aID, accountFields[a]) + " in " + JSON.stringify(accountValues[a]) + " ? " + accountmatch);
+ }
+
+ if (accountmatch) {
+ for (let fID in this.folders[aID]) {
+ //does this folder match folder search options?
+ let foldermatch = true;
+ for (let f = 0; f < folderFields.length && foldermatch; f++) {
+ foldermatch = folderValues[f].some(item => item === this.getFolderProperty(aID, fID, folderFields[f]));
+ //Services.console.logStringMessage(" " + folderFields[f] + ":" + this.getFolderProperty(aID, fID, folderFields[f]) + " in " + JSON.stringify(folderValues[f]) + " ? " + foldermatch);
+ }
+ if (foldermatch) data.push({accountID: aID, folderID: fID, data: this.folders[aID][fID]});
+ }
+ }
+ }
+
+ //still a reference to the original data
+ return data;
+ }
+};