diff options
Diffstat (limited to '')
-rw-r--r-- | content/modules/public.js | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/content/modules/public.js b/content/modules/public.js new file mode 100644 index 0000000..afd4f03 --- /dev/null +++ b/content/modules/public.js @@ -0,0 +1,757 @@ +/* + * 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 StatusData = class { + /** + * A StatusData instance must be used as return value by + * :class:`Base.syncFolderList` and :class:`Base.syncFolder`. + * + * StatusData also defines the possible StatusDataTypes used by the + * :ref:`TbSyncEventLog`. + * + * @param {StatusDataType} type Status type (see const definitions below) + * @param {string} message ``Optional`` A message, which will be used as + * sync status. If this is not a success, it will be + * used also in the :ref:`TbSyncEventLog` as well. + * @param {string} details ``Optional`` If this is not a success, it will + * be used as description in the + * :ref:`TbSyncEventLog`. + * + */ + constructor(type = "success", message = "", details = "") { + this.type = type; //success, info, warning, error + this.message = message; + this.details = details; + } + /** + * Successfull sync. + */ + static get SUCCESS() {return "success"}; + /** + * Sync of the entire account will be aborted. + */ + static get ERROR() {return "error"}; + /** + * Sync of this resource will be aborted and continued with next resource. + */ + static get WARNING() {return "warning"}; + /** + * Successfull sync, but message and details + * provided will be added to the event log. + */ + static get INFO() {return "info"}; + /** + * Sync of the entire account will be aborted and restarted completely. + */ + static get ACCOUNT_RERUN() {return "account_rerun"}; + /** + * Sync of the current folder/resource will be restarted. + */ + static get FOLDER_RERUN() {return "folder_rerun"}; +} + + + +/** + * ProgressData to manage a ``done`` and a ``todo`` counter. + * + * Each :class:`SyncData` instance has an associated ProgressData instance. See + * :class:`SyncData.progressData`. The information of that ProgressData + * instance is used, when the current syncstate is prefixed by ``send.``, + * ``eval.`` or ``prepare.``. See :class:`SyncData.setSyncState`. + * + */ +var ProgressData = class { + /** + * + */ + constructor() { + this._todo = 0; + this._done = 0; + } + + /** + * Reset ``done`` and ``todo`` counter. + * + * @param {integer} done ``Optional`` Set a value for the ``done`` counter. + * @param {integer} todo ``Optional`` Set a value for the ``todo`` counter. + * + */ + reset(done = 0, todo = 0) { + this._todo = todo; + this._done = done; + } + + /** + * Increment the ``done`` counter. + * + * @param {integer} value ``Optional`` Set incrementation value. + * + */ + inc(value = 1) { + this._done += value; + } + + /** + * Getter for the ``todo`` counter. + * + */ + get todo() { + return this._todo; + } + + /** + * Getter for the ``done`` counter. + * + */ + get done() { + return this._done; + } +} + + + +/** + * ProviderData + * + */ +var ProviderData = class { + /** + * Constructor + * + * @param {FolderData} folderData FolderData of the folder for which the + * display name is requested. + * + */ + constructor(provider) { + if (!TbSync.providers.hasOwnProperty(provider)) { + throw new Error("Provider <" + provider + "> has not been loaded. Failed to create ProviderData."); + } + this.provider = provider; + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this ProviderData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.getAccountProperty("provider")); + } + + getVersion() { + return TbSync.providers.loadedProviders[this.provider].version; + } + + get extension() { + return TbSync.providers.loadedProviders[this.provider].extension; + } + + getAllAccounts() { + let accounts = TbSync.db.getAccounts(); + let allAccounts = []; + for (let i=0; i<accounts.IDs.length; i++) { + let accountID = accounts.IDs[i]; + if (accounts.data[accountID].provider == this.provider) { + allAccounts.push(new TbSync.AccountData(accountID)); + } + } + return allAccounts; + } + + getFolders(aFolderSearchCriteria = {}) { + let allFolders = []; + let folderSearchCriteria = {}; + Object.assign(folderSearchCriteria, aFolderSearchCriteria); + folderSearchCriteria.cached = false; + + let folders = TbSync.db.findFolders(folderSearchCriteria, {"provider": this.provider}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(new TbSync.AccountData(folders[i].accountID), folders[i].folderID)); + } + return allFolders; + } + + getDefaultAccountEntries() { + return TbSync.providers.getDefaultAccountEntries(this.provider) + } + + addAccount(accountName, accountOptions) { + let newAccountID = TbSync.db.addAccount(accountName, accountOptions); + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountsList", newAccountID); + return new TbSync.AccountData(newAccountID); + } +} + + + +/** + * AccountData + * + */ +var AccountData = class { + /** + * + */ + constructor(accountID) { + this._accountID = accountID; + + if (!TbSync.db.accounts.data.hasOwnProperty(accountID)) { + throw new Error("An account with ID <" + accountID + "> does not exist. Failed to create AccountData."); + } + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this AccountData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.getAccountProperty("provider"), + this.getAccountProperty("accountname"), + this.accountID); + } + + get accountID() { + return this._accountID; + } + + getAllFolders() { + let allFolders = []; + let folders = TbSync.db.findFolders({"cached": false}, {"accountID": this.accountID}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(this, folders[i].folderID)); + } + return allFolders; + } + + getAllFoldersIncludingCache() { + let allFolders = []; + let folders = TbSync.db.findFolders({}, {"accountID": this.accountID}); + for (let i=0; i < folders.length; i++) { + allFolders.push(new TbSync.FolderData(this, folders[i].folderID)); + } + return allFolders; + } + + getFolder(setting, value) { + // ES6 supports variable keys by putting it into brackets + let folders = TbSync.db.findFolders({[setting]: value, "cached": false}, {"accountID": this.accountID}); + if (folders.length > 0) return new TbSync.FolderData(this, folders[0].folderID); + return null; + } + + getFolderFromCache(setting, value) { + // ES6 supports variable keys by putting it into brackets + let folders = TbSync.db.findFolders({[setting]: value, "cached": true}, {"accountID": this.accountID}); + if (folders.length > 0) return new TbSync.FolderData(this, folders[0].folderID); + return null; + } + + createNewFolder() { + return new TbSync.FolderData(this, TbSync.db.addFolder(this.accountID)); + } + + // get data objects + get providerData() { + return new TbSync.ProviderData( + this.getAccountProperty("provider"), + ); + } + + get syncData() { + return TbSync.core.getSyncDataObject(this.accountID); + } + + + /** + * Initiate a sync of this entire account by calling + * :class:`Base.syncFolderList`. If that succeeded, :class:`Base.syncFolder` + * will be called for each available folder / resource found on the server. + * + * @param {Object} syncDescription ``Optional`` + */ + sync(syncDescription = {}) { + TbSync.core.syncAccount(this.accountID, syncDescription); + } + + isSyncing() { + return TbSync.core.isSyncing(this.accountID); + } + + isEnabled() { + return TbSync.core.isEnabled(this.accountID); + } + + isConnected() { + return TbSync.core.isConnected(this.accountID); + } + + + getAccountProperty(field) { + return TbSync.db.getAccountProperty(this.accountID, field); + } + + setAccountProperty(field, value) { + TbSync.db.setAccountProperty(this.accountID, field, value); + Services.obs.notifyObservers(null, "tbsync.observer.manager.reloadAccountSetting", JSON.stringify({accountID: this.accountID, setting: field})); + } + + resetAccountProperty(field) { + TbSync.db.resetAccountProperty(this.accountID, field); + Services.obs.notifyObservers(null, "tbsync.observer.manager.reloadAccountSetting", JSON.stringify({accountID: this.accountID, setting: field})); + } +} + + + +/** + * FolderData + * + */ +var FolderData = class { + /** + * + */ + constructor(accountData, folderID) { + this._accountData = accountData; + this._folderID = folderID; + this._target = null; + + if (!TbSync.db.folders[accountData.accountID].hasOwnProperty(folderID)) { + throw new Error("A folder with ID <" + folderID + "> does not exist for the given account. Failed to create FolderData."); + } + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this FolderData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.accountData.getAccountProperty("provider"), + this.accountData.getAccountProperty("accountname"), + this.accountData.accountID, + this.getFolderProperty("foldername"), + ); + } + + get folderID() { + return this._folderID; + } + + get accountID() { + return this._accountData.accountID; + } + + getDefaultFolderEntries() { // remove + return TbSync.providers.getDefaultFolderEntries(this.accountID); + } + + getFolderProperty(field) { + return TbSync.db.getFolderProperty(this.accountID, this.folderID, field); + } + + setFolderProperty(field, value) { + TbSync.db.setFolderProperty(this.accountID, this.folderID, field, value); + } + + resetFolderProperty(field) { + TbSync.db.resetFolderProperty(this.accountID, this.folderID, field); + } + + /** + * Initiate a sync of this folder only by calling + * :class:`Base.syncFolderList` and than :class:`Base.syncFolder` for this + * folder / resource only. + * + * @param {Object} syncDescription ``Optional`` + */ + sync(aSyncDescription = {}) { + let syncDescription = {}; + Object.assign(syncDescription, aSyncDescription); + + syncDescription.syncFolders = [this]; + this.accountData.sync(syncDescription); + } + + isSyncing() { + let syncdata = this.accountData.syncData; + return (syncdata.currentFolderData && syncdata.currentFolderData.folderID == this.folderID); + } + + getFolderStatus() { + let status = ""; + + if (this.getFolderProperty("selected")) { + //default + status = TbSync.getString("status." + this.getFolderProperty("status"), this.accountData.getAccountProperty("provider")).split("||")[0]; + + switch (this.getFolderProperty("status").split(".")[0]) { //the status may have a sub-decleration + case "modified": + //trigger periodic sync (TbSync.syncTimer, tbsync.jsm) + if (!this.isSyncing()) { + this.accountData.setAccountProperty("lastsynctime", 0); + } + case "success": + try { + status = status + ": " + this.targetData.targetName; + } catch (e) { + this.resetFolderProperty("target"); + this.setFolderProperty("status","notsyncronized"); + return TbSync.getString("status.notsyncronized"); + } + break; + + case "pending": + //add extra info if this folder is beeing synced + if (this.isSyncing()) { + let syncdata = this.accountData.syncData; + status = TbSync.getString("status.syncing", this.accountData.getAccountProperty("provider")); + if (["send","eval","prepare"].includes(syncdata.getSyncState().state.split(".")[0]) && (syncdata.progressData.todo + syncdata.progressData.done) > 0) { + //add progress information + status = status + " (" + syncdata.progressData.done + (syncdata.progressData.todo > 0 ? "/" + syncdata.progressData.todo : "") + ")"; + } + } + break; + } + } else { + //remain empty if not selected + } + return status; + } + + // get data objects + get accountData() { + return this._accountData; + } + + /** + * Getter for the :class:`TargetData` instance associated with this + * FolderData. See :ref:`TbSyncTargets` for more details. + * + * @returns {TargetData} + * + */ + get targetData() { + // targetData is created on demand + if (!this._target) { + let provider = this.accountData.getAccountProperty("provider"); + let targetType = this.getFolderProperty("targetType"); + + if (!targetType) + throw new Error("Provider <"+provider+"> has not set a proper target type for this folder."); + + if (!TbSync.providers[provider].hasOwnProperty("TargetData_" + targetType)) + throw new Error("Provider <"+provider+"> is missing a TargetData implementation for <"+targetType+">."); + + this._target = new TbSync.providers[provider]["TargetData_" + targetType](this); + + if (!this._target) + throw new Error("notargets"); + } + + return this._target; + } + + // Removes the folder and its target. If the target should be + // kept as a stale/unconnected item, provide a suffix, which + // will be added to its name, to indicate, that it is no longer + // managed by TbSync. + remove(keepStaleTargetSuffix = "") { + // hasTarget() can throw an error, ignore that here + try { + if (this.targetData.hasTarget()) { + if (keepStaleTargetSuffix) { + let oldName = this.targetData.targetName; + this.targetData.targetName = TbSync.getString("target.orphaned") + ": " + oldName + " " + keepStaleTargetSuffix; + this.targetData.disconnectTarget(); + } else { + this.targetData.removeTarget(); + } + } + } catch (e) { + Components.utils.reportError(e); + } + this.setFolderProperty("cached", true); + } +} + + + +/** + * There is only one SyncData instance per account which contains all + * relevant information regarding an ongoing sync. + * + */ +var SyncData = class { + /** + * + */ + constructor(accountID) { + + //internal (private, not to be touched by provider) + this._syncstate = { + state: "accountdone", + timestamp: Date.now(), + } + this._accountData = new TbSync.AccountData(accountID); + this._progressData = new TbSync.ProgressData(); + this._currentFolderData = null; + } + + //all functions provider should use should be in here + //providers should not modify properties directly + //when getSyncDataObj is used never change the folder id as a sync may be going on! + + _setCurrentFolderData(folderData) { + this._currentFolderData = folderData; + } + _clearCurrentFolderData() { + this._currentFolderData = null; + } + + /** + * Getter for an :class:`EventLogInfo` instance with all the information + * regarding this SyncData instance. + * + */ + get eventLogInfo() { + return new EventLogInfo( + this.accountData.getAccountProperty("provider"), + this.accountData.getAccountProperty("accountname"), + this.accountData.accountID, + this.currentFolderData ? this.currentFolderData.getFolderProperty("foldername") : "", + ); + } + + /** + * Getter for the :class:`FolderData` instance of the folder being currently + * synced. Can be ``null`` if no folder is being synced. + * + */ + get currentFolderData() { + return this._currentFolderData; + } + + /** + * Getter for the :class:`AccountData` instance of the account being + * currently synced. + * + */ + get accountData() { + return this._accountData; + } + + /** + * Getter for the :class:`ProgressData` instance of the ongoing sync. + * + */ + get progressData() { + return this._progressData; + } + + /** + * Sets the syncstate of the ongoing sync, to provide feedback to the user. + * The selected state can trigger special UI features, if it starts with one + * of the following prefixes: + * + * * ``send.``, ``eval.``, ``prepare.`` : + * The status message in the UI will be appended with the current progress + * stored in the :class:`ProgressData` associated with this SyncData + * instance. See :class:`SyncData.progressData`. + * + * * ``send.`` : + * The status message in the UI will be appended by a timeout countdown + * with the timeout being defined by :class:`Base.getConnectionTimeout`. + * + * @param {string} state A short syncstate identifier. The actual + * message to be displayed in the UI will be + * looked up in the locales of the provider + * by looking for ``syncstate.<state>``. + * The lookup is done via :func:`getString`, + * so the same fallback rules apply. + * + */ + setSyncState(state) { + //set new syncstate + let msg = "State: " + state + ", Account: " + this.accountData.getAccountProperty("accountname"); + if (this.currentFolderData) msg += ", Folder: " + this.currentFolderData.getFolderProperty("foldername"); + + let syncstate = {}; + syncstate.state = state; + syncstate.timestamp = Date.now(); + + this._syncstate = syncstate; + TbSync.dump("setSyncState", msg); + + Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", this.accountData.accountID); + } + + /** + * Gets the current syncstate and its timestamp of the ongoing sync. The + * returned Object has the following attributes: + * + * * ``state`` : the current syncstate + * * ``timestamp`` : its timestamp + * + * @returns {Object} The syncstate and its timestamp. + * + */ + getSyncState() { + return this._syncstate; + } +} + + + + + + + + + + +// Simple dumper, who can dump to file or console +// It is suggested to use the event log instead of dumping directly. +var dump = function (what, aMessage) { + if (TbSync.prefs.getBoolPref("log.toconsole")) { + Services.console.logStringMessage("[TbSync] " + what + " : " + aMessage); + } + + if (TbSync.prefs.getIntPref("log.userdatalevel") > 0) { + let now = new Date(); + TbSync.io.appendToFile("debug.log", "** " + now.toString() + " **\n[" + what + "] : " + aMessage + "\n\n"); + } +} + + + +/** + * Get a localized string. + * + * TODO: Explain placeholder and :: notation. + * + * @param {string} key The key of the message to look up + * @param {string} provider ``Optional`` The provider the key belongs to. + * + * @returns {string} The message belonging to the key of the specified provider. + * If that key is not found in the in the specified provider + * or if no provider has been specified, the messages of + * TbSync itself we be used as fallback. If the key could not + * be found there as well, the key itself is returned. + * + */ +var getString = function (key, provider) { + let localized = null; + + //spezial treatment of strings with :: like status.httperror::403 + let parts = key.split("::"); + + // if a provider is given, try to get the string from the provider + if (provider && TbSync.providers.loadedProviders.hasOwnProperty(provider)) { + let localeData = TbSync.providers.loadedProviders[provider].extension.localeData; + if (localeData.messages.get(localeData.selectedLocale).has(parts[0].toLowerCase())) { + localized = TbSync.providers.loadedProviders[provider].extension.localeData.localizeMessage(parts[0]); + } + } + + // if we did not yet succeed, check the locales of tbsync itself + if (!localized) { + localized = TbSync.extension.localeData.localizeMessage(parts[0]); + } + + if (!localized) { + localized = key; + } else { + //replace placeholders in returned string + for (let i = 0; i<parts.length; i++) { + let regex = new RegExp( "##replace\."+i+"##", "g"); + localized = localized.replace(regex, parts[i]); + } + } + + return localized; +} + + +var localizeNow = function (window, provider) { + let document = window.document; + let keyPrefix = "__" + (provider ? provider.toUpperCase() + "4" : "") + "TBSYNCMSG_"; + + let localization = { + i18n: null, + + updateString(string) { + let re = new RegExp(keyPrefix + "(.+?)__", "g"); + return string.replace(re, matched => { + const key = matched.slice(keyPrefix.length, -2); + return TbSync.getString(key, provider) || matched; + }); + }, + + updateDocument(node) { + const texts = document.evaluate( + 'descendant::text()[contains(self::text(), "' + keyPrefix + '")]', + node, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) { + const text = texts.snapshotItem(i); + if (text.nodeValue.includes(keyPrefix)) text.nodeValue = this.updateString(text.nodeValue); + } + + const attributes = document.evaluate( + 'descendant::*/attribute::*[contains(., "' + keyPrefix + '")]', + node, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) { + const attribute = attributes.snapshotItem(i); + if (attribute.value.includes(keyPrefix)) attribute.value = this.updateString(attribute.value); + } + } + }; + + localization.updateDocument(document); +} + +var localizeOnLoad = function (window, provider) { + // standard event if loaded by a standard window + window.document.addEventListener('DOMContentLoaded', () => { + this.localizeNow(window, provider); + }, { once: true }); + + // custom event, fired by the overlay loader after it has finished loading + // the editAccount dialog is never called as a provider, but from tbsync itself + let eventId = "DOMOverlayLoaded_" + + (!provider || window.location.href.startsWith("chrome://tbsync/content/manager/editAccount.") ? "" : provider + "4") + + "tbsync@jobisoft.de"; + window.document.addEventListener(eventId, () => { + TbSync.localizeNow(window, provider); + }, { once: true }); +} + + + +var generateUUID = function () { + const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + return uuidGenerator.generateUUID().toString().replace(/[{}]/g, ''); +} |