summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 06:29:37 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 06:29:37 +0000
commit9355e23a909a7801b3ccdf68ee05b3480be42407 (patch)
tree78220623341b88bd42d16929e2acb3e5ef52e0c8 /content
parentInitial commit. (diff)
downloadtbsync-9355e23a909a7801b3ccdf68ee05b3480be42407.tar.xz
tbsync-9355e23a909a7801b3ccdf68ee05b3480be42407.zip
Adding upstream version 4.7.upstream/4.7
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--content/HttpRequest.jsm755
-rw-r--r--content/OverlayManager.jsm514
-rw-r--r--content/api/BootstrapLoader/CHANGELOG.md75
-rw-r--r--content/api/BootstrapLoader/README.md1
-rw-r--r--content/api/BootstrapLoader/implementation.js917
-rw-r--r--content/api/BootstrapLoader/schema.json61
-rw-r--r--content/manager/accountManager.js140
-rw-r--r--content/manager/accountManager.xhtml27
-rw-r--r--content/manager/accounts.js498
-rw-r--r--content/manager/accounts.xhtml119
-rw-r--r--content/manager/addonoptions.xhtml15
-rw-r--r--content/manager/catman.xhtml25
-rw-r--r--content/manager/editAccount.js391
-rw-r--r--content/manager/editAccount.xhtml87
-rw-r--r--content/manager/eventlog/eventlog.js158
-rw-r--r--content/manager/eventlog/eventlog.xhtml21
-rw-r--r--content/manager/help.xhtml58
-rw-r--r--content/manager/installProvider.xhtml35
-rw-r--r--content/manager/manageProvider.js40
-rw-r--r--content/manager/manager.css38
-rw-r--r--content/manager/missingProvider.xhtml21
-rw-r--r--content/manager/noaccounts.xhtml17
-rw-r--r--content/manager/support-wizard/support-wizard.xhtml44
-rw-r--r--content/manager/supporter.xhtml77
-rw-r--r--content/modules/addressbook.js1149
-rw-r--r--content/modules/core.js332
-rw-r--r--content/modules/db.js460
-rw-r--r--content/modules/eventlog.js153
-rw-r--r--content/modules/io.js41
-rw-r--r--content/modules/lightning.js774
-rw-r--r--content/modules/manager.js392
-rw-r--r--content/modules/messenger.js99
-rw-r--r--content/modules/network.js114
-rw-r--r--content/modules/passwordManager.js78
-rw-r--r--content/modules/providers.js184
-rw-r--r--content/modules/public.js757
-rw-r--r--content/modules/tools.js80
-rw-r--r--content/overlays/messenger.js22
-rw-r--r--content/overlays/messenger.xhtml20
-rw-r--r--content/passwordPrompt/passwordPrompt.css13
-rw-r--r--content/passwordPrompt/passwordPrompt.js47
-rw-r--r--content/passwordPrompt/passwordPrompt.xhtml32
-rw-r--r--content/scripts/bootstrap.js89
-rw-r--r--content/scripts/locales.js3
-rw-r--r--content/skin/ab.css8
-rw-r--r--content/skin/acl_ro.pngbin0 -> 15607 bytes
-rw-r--r--content/skin/acl_ro2.pngbin0 -> 18264 bytes
-rw-r--r--content/skin/acl_rw.pngbin0 -> 15519 bytes
-rw-r--r--content/skin/acl_rw2.pngbin0 -> 17601 bytes
-rw-r--r--content/skin/add16.pngbin0 -> 17963 bytes
-rw-r--r--content/skin/browserOverlay.css10
-rw-r--r--content/skin/calendar16.pngbin0 -> 18192 bytes
-rw-r--r--content/skin/calendar16_shared.pngbin0 -> 18776 bytes
-rw-r--r--content/skin/catman32.pngbin0 -> 19134 bytes
-rw-r--r--content/skin/connect16.pngbin0 -> 1370 bytes
-rw-r--r--content/skin/contacts16.pngbin0 -> 18435 bytes
-rw-r--r--content/skin/contacts16_shared.pngbin0 -> 19055 bytes
-rw-r--r--content/skin/del16.pngbin0 -> 18172 bytes
-rw-r--r--content/skin/disabled16.pngbin0 -> 15506 bytes
-rw-r--r--content/skin/eas16.pngbin0 -> 664 bytes
-rw-r--r--content/skin/error16.pngbin0 -> 696 bytes
-rw-r--r--content/skin/fix_dropdown_1534697.css4
-rw-r--r--content/skin/group32.pngbin0 -> 433 bytes
-rw-r--r--content/skin/help32.pngbin0 -> 4667 bytes
-rw-r--r--content/skin/info16.pngbin0 -> 549 bytes
-rw-r--r--content/skin/lock24.pngbin0 -> 646 bytes
-rw-r--r--content/skin/provider16.pngbin0 -> 837 bytes
-rw-r--r--content/skin/provider32.pngbin0 -> 2166 bytes
-rw-r--r--content/skin/report_open.pngbin0 -> 21829 bytes
-rw-r--r--content/skin/report_send.pngbin0 -> 21829 bytes
-rw-r--r--content/skin/settings32.pngbin0 -> 1535 bytes
-rw-r--r--content/skin/slider-off.pngbin0 -> 17567 bytes
-rw-r--r--content/skin/slider-on.pngbin0 -> 17586 bytes
-rw-r--r--content/skin/spinner.gifbin0 -> 1849 bytes
-rw-r--r--content/skin/src/LICENSE131
-rw-r--r--content/skin/src/slider-off.pngbin0 -> 17567 bytes
-rw-r--r--content/skin/src/slider-on.pngbin0 -> 17586 bytes
-rw-r--r--content/skin/src/slider.xcfbin0 -> 2970 bytes
-rw-r--r--content/skin/sync16.pngbin0 -> 18091 bytes
-rw-r--r--content/skin/sync16_1.pngbin0 -> 18101 bytes
-rw-r--r--content/skin/sync16_2.pngbin0 -> 18078 bytes
-rw-r--r--content/skin/sync16_3.pngbin0 -> 18091 bytes
-rw-r--r--content/skin/sync16_4.pngbin0 -> 18102 bytes
-rw-r--r--content/skin/tbsync.pngbin0 -> 1121 bytes
-rw-r--r--content/skin/tbsync64.pngbin0 -> 2206 bytes
-rw-r--r--content/skin/tick16.pngbin0 -> 1324 bytes
-rw-r--r--content/skin/todo16.pngbin0 -> 650 bytes
-rw-r--r--content/skin/todo16_share.pngbin0 -> 818 bytes
-rw-r--r--content/skin/trash16.pngbin0 -> 774 bytes
-rw-r--r--content/skin/update32.pngbin0 -> 398 bytes
-rw-r--r--content/skin/user48.pngbin0 -> 442 bytes
-rw-r--r--content/skin/warning16.pngbin0 -> 483 bytes
-rw-r--r--content/tbsync.jsm163
93 files changed, 9289 insertions, 0 deletions
diff --git a/content/HttpRequest.jsm b/content/HttpRequest.jsm
new file mode 100644
index 0000000..fa28f97
--- /dev/null
+++ b/content/HttpRequest.jsm
@@ -0,0 +1,755 @@
+/*
+ * This file is part of TbSync, contributed by John Bieling.
+ *
+ * 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/.
+ *
+ * Limitations:
+ * ============
+ * - no real event support (cannot add eventlisteners)
+ * - send only supports string body
+ * - onprogress not supported
+ * - readyState 2 & 3 not supported
+ *
+ * Note about HttpRequest.open(method, url, async, username, password):
+ * ============================================================================
+ * If an Authorization header is specified, HttpRequest will use the
+ * given header.
+ *
+ * If no Authorization header is specified, but a username, HttpRequest
+ * will delegate the authentication process to nsIHttpChannel. If a password is
+ * specified as well, it will be used for authentication. If no password is
+ * specified, it will call the passwordCallback(username, realm, host) callback to
+ * request a password for the given username, host and realm send back from
+ * the server (in the WWW-Authenticate header).
+ *
+ */
+
+ "use strict";
+
+var EXPORTED_SYMBOLS = ["HttpRequest"];
+
+var bug669675 = [];
+var containers = [];
+var sandboxes = {};
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var HttpRequest = class {
+ constructor() {
+ // a private object to store xhr related properties
+ this._xhr = {};
+
+ // HttpRequest supports two methods to receive data, using the
+ // streamLoader seems to be the more modern approach.
+ // BUT in order to overide MimeType, we need to call onStartRequest
+ this._xhr.useStreamLoader = false;
+ this._xhr.responseAsBase64 = false;
+
+ this._xhr.loadFlags = Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
+ this._xhr.headers = {};
+ this._xhr.readyState = 0;
+ this._xhr.responseStatus = null;
+ this._xhr.responseStatusText = null;
+ this._xhr.responseText = null;
+ this._xhr.httpchannel = null;
+ this._xhr.method = null;
+ this._xhr.uri = null;
+ this._xhr.permanentlyRedirectedUrl = null;
+ this._xhr.username = "";
+ this._xhr.password = "";
+ this._xhr.overrideMimeType = null;
+ this._xhr.mozAnon = false;
+ this._xhr.mozBackgroundRequest = false;
+ this._xhr.timeout = 0;
+ this._xhr.redirectFlags = null;
+ this._xhr.containerReset = false;
+ this._xhr.containerRealm = "default";
+
+ this.onreadystatechange = function () {};
+ this.onerror = function () {};
+ this.onload = function () {};
+ this.ontimeout = function () {};
+
+ // Redirects are handled internally, this callback is just called to
+ // inform the caller about the redirect.
+ // Flags: (https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIChannelEventSink)
+ // - REDIRECT_TEMPORARY = 1 << 0;
+ // - REDIRECT_PERMANENT = 1 << 1;
+ // - REDIRECT_INTERNAL = 1 << 2;
+ // TB enforces a redirect from http -> https without having received a redirect,
+ // but an STS header
+ // - REDIRECT_STS_UPGRADE = 1 << 3;
+ // This is a custom bit set by HttpRequest to indicate, that this redirect was missed by the nsIHttpChannel implementation
+ // probably due to a CORS violation (like https -> http)
+ // - REDIRECT_MISSED = 1 << 7;
+ this.onredirect = function(flags, newUri) {};
+
+ // Whenever a WWW-Authenticate header has been parsed, this callback is
+ // called to inform the caller about the found realm.
+ this.realmCallback = function (username, realm, host) {};
+
+ // Whenever a channel needs authentication, but the caller has only provided a username
+ // this callback is called to request the password.
+ this.passwordCallback = function (username, realm, host) {return null};
+
+ var self = this;
+
+ this.notificationCallbacks = {
+ // nsIInterfaceRequestor
+ getInterface : function(aIID) {
+ if (aIID.equals(Components.interfaces.nsIAuthPrompt2)) {
+ // implement a custom nsIAuthPrompt2 - needed for auto authorization
+ if (!self._xhr.authPrompt) {
+ self._xhr.authPrompt = new HttpRequestPrompt(self._xhr.username, self._xhr.password, self.passwordCallback, self.realmCallback);
+ }
+ return self._xhr.authPrompt;
+ } else if (aIID.equals(Components.interfaces.nsIAuthPrompt)) {
+ // implement a custom nsIAuthPrompt
+ } else if (aIID.equals(Components.interfaces.nsIAuthPromptProvider)) {
+ // implement a custom nsIAuthPromptProvider
+ } else if (aIID.equals(Components.interfaces.nsIPrompt)) {
+ // implement a custom nsIPrompt
+ } else if (aIID.equals(Components.interfaces.nsIProgressEventSink)) {
+ // implement a custom nsIProgressEventSink
+ } else if (aIID.equals(Components.interfaces.nsIChannelEventSink)) {
+ // implement a custom nsIChannelEventSink
+ return self.redirect;
+ }
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+ };
+
+ this.redirect = {
+ // nsIChannelEventSink implementation
+ asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
+ // Disallow redirects from https to http.
+ if (aOldChannel.URI.scheme == "https" && aNewChannel.URI.scheme == "http") {
+ // Using an unused error code according to https://developer.mozilla.org/en-US/docs/Mozilla/Errors.
+ // REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP'
+ aCallback.onRedirectVerifyCallback(0x804B002F);
+ return;
+ }
+
+ let uploadData;
+ let uploadContent;
+ if (aOldChannel instanceof Ci.nsIUploadChannel &&
+ aOldChannel instanceof Ci.nsIHttpChannel &&
+ aOldChannel.uploadStream) {
+ uploadData = aOldChannel.uploadStream;
+ uploadContent = aOldChannel.getRequestHeader("Content-Type");
+ }
+
+ aNewChannel.QueryInterface(Ci.nsIHttpChannel);
+ aOldChannel.QueryInterface(Ci.nsIHttpChannel);
+
+ function copyHeader(aHdr) {
+ try {
+ let hdrValue = aOldChannel.getRequestHeader(aHdr);
+ if (hdrValue) {
+ aNewChannel.setRequestHeader(aHdr, hdrValue, false);
+ }
+ } catch (e) {
+ if (e.code != Components.results.NS_ERROR_NOT_AVAILIBLE) {
+ // The header could possibly not be available, ignore that
+ // case but throw otherwise
+ throw e;
+ }
+ }
+ }
+
+ // Copy manually added headers
+ for (let header in self._xhr.headers) {
+ if (self._xhr.headers.hasOwnProperty(header)) {
+ copyHeader(header);
+ }
+ }
+
+ prepHttpChannelUploadData(
+ aNewChannel,
+ aOldChannel.requestMethod,
+ uploadData,
+ uploadContent);
+
+ self._xhr.redirectFlags = aFlags;
+ if (aFlags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) {
+ self._xhr.permanentlyRedirectedUrl = aNewChannel.URI.spec;
+ }
+ self.onredirect(aFlags, aNewChannel.URI);
+ aCallback.onRedirectVerifyCallback(Components.results.NS_OK);
+ }
+ };
+
+ this.listener = {
+ _buffer: [],
+
+ //nsIStreamListener (aUseStreamLoader = false)
+ onStartRequest: function(aRequest) {
+ //Services.console.logStringMessage("[onStartRequest] " + aRequest.URI.spec);
+ this._buffer = [];
+
+ if (self._xhr.overrideMimeType) {
+ aRequest.contentType = self._xhr.overrideMimeType;
+ }
+ },
+ onDataAvailable: function (aRequest, aInputStream, aOffset, aCount) {
+ //Services.console.logStringMessage("[onDataAvailable] " + aRequest.URI.spec + " : " + aCount);
+ let buffer = new ArrayBuffer(aCount);
+ let stream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
+ stream.setInputStream(aInputStream);
+ stream.readArrayBuffer(aCount, buffer);
+
+ // store the chunk
+ this._buffer.push(Array.from(new Uint8Array(buffer)));
+ },
+ onStopRequest: function(aRequest, aStatusCode) {
+ //Services.console.logStringMessage("[onStopRequest] " + aRequest.URI.spec + " : " + aStatusCode);
+ // combine all binary chunks to create a flat byte array;
+ let combined = [].concat.apply([], this._buffer);
+ let data = convertByteArray(combined, self.responseAsBase64);
+ this.processResponse(aRequest.QueryInterface(Components.interfaces.nsIHttpChannel), aStatusCode, data);
+ },
+
+
+
+ //nsIStreamLoaderObserver (aUseStreamLoader = true)
+ onStreamComplete: function(aLoader, aContext, aStatus, aResultLength, aResult) {
+ let result = convertByteArray(aResult, self.responseAsBase64);
+ this.processResponse(aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel), aStatus, result);
+ },
+
+ processResponse: function(aChannel, aStatus, aResult) {
+ //Services.console.logStringMessage("[processResponse] " + aChannel.URI.spec + " : " + aStatus);
+ // do not set any channal response data, before we know we failed
+ // and before we know we do not have to rerun (due to bug 669675)
+
+ let responseStatus = null;
+ try {
+ responseStatus = aChannel.responseStatus;
+ } catch (ex) {
+ switch (aStatus) {
+ case Components.results.NS_ERROR_NET_TIMEOUT:
+ self._xhr.httpchannel = aChannel;
+ self._xhr.responseText = aResult;
+ self._xhr.responseStatus = 0;
+ self._xhr.responseStatusText = "";
+ self._xhr.readyState = 4;
+ self.onreadystatechange();
+ self.ontimeout();
+ return;
+ case Components.results.NS_BINDING_ABORTED:
+ case 0x804B002F: //Custom error (REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP')
+ self._xhr.httpchannel = aChannel;
+ self._xhr.responseText = aResult;
+ self._xhr.responseStatus = 0;
+ self._xhr.responseStatusText = "";
+ self._xhr.readyState = 0;
+ self.onreadystatechange();
+ self.onerror();
+ return;
+ case 0x805303F4: //NS_ERROR_DOM_BAD_URI
+ // Error on Strict-Transport-Security induced Redirect http -> https (these do not even show up in the console)
+ // The redirect has been done already and the channel is already the new channel.
+ // Due to CORS violation, it cannot be fulfilled.
+ if (self._xhr.redirectFlags && aChannel.URI.spec != self._xhr.uri.spec) {
+ self._xhr.uri = aChannel.URI;
+ self.send(self._xhr.data);
+ return;
+ }
+ default:
+ self._xhr.httpchannel = aChannel;
+ self._xhr.responseText = aResult;
+ self._xhr.responseStatus = 0;
+ self._xhr.responseStatusText = "";
+ self._xhr.readyState = 4;
+ self.onreadystatechange();
+ self.onerror();
+ return;
+ }
+ }
+
+ // Usually redirects are handled internally, but any CORS violating request is
+ // returning a 30x, if CORS is not allowed. This is not true for STS induced redirects (see above).
+ if ([301,302,307,308].includes(responseStatus)) {
+ // aChannel is still the old channel
+ let redirected = self.getResponseHeader("location");
+ if (redirected && redirected != aChannel.URI.spec) {
+ let flag = Ci.nsIChannelEventSink.REDIRECT_TEMPORARY;
+ let uri = Services.io.newURI(redirected);
+ if ([301,308].includes(responseStatus)) {
+ flag = Ci.nsIChannelEventSink.REDIRECT_PERMANENT;
+ self._xhr.permanentlyRedirectedUrl = uri.spec;
+ }
+ // inform caller about the redirect and set the REDIRECT_MISSED bit
+
+ self.onredirect(flag | 0x80, uri);
+ self._xhr.uri = uri;
+ self.send(self._xhr.data);
+ return;
+ }
+ }
+
+ // mitigation for bug https://bugzilla.mozilla.org/show_bug.cgi?id=669675
+ // we need to check, if nsIHttpChannel was in charge of auth:
+ // if there was no Authentication header provided by the user, but a username
+ // nsIHttpChannel should have added one. Is there one?
+ if (
+ (responseStatus == 401) &&
+ !self._xhr.mozAnon &&
+ !self.hasRequestHeader("Authorization") && // no user defined header, so nsIHttpChannel should have called the authPrompt
+ self._xhr.username && // we can only add basic auth header if user
+ self._xhr.password // and pass are present
+ ) {
+ // check the actual Authorization headers send
+ let unauthenticated;
+ try {
+ let header = aChannel.getRequestHeader("Authorization");
+ unauthenticated = false;
+ } catch (e) {
+ unauthenticated = true;
+ }
+
+ if (unauthenticated) {
+ if (!bug669675.includes(self._xhr.uri.spec)) {
+ bug669675.push(self._xhr.uri.spec)
+ console.log("Mitigation for bug 669675 for URL <"+self._xhr.uri.spec+"> (Once per URL per session)");
+ // rerun
+ self.send(self._xhr.data);
+ return;
+ } else {
+ console.log("Mitigation failed for URL <"+self._xhr.uri.spec+">");
+ }
+ }
+ }
+
+ self._xhr.httpchannel = aChannel;
+ self._xhr.responseText = aResult;
+ self._xhr.responseStatus = responseStatus;
+ self._xhr.responseStatusText = aChannel.responseStatusText;
+ self._xhr.readyState = 4;
+ self.onreadystatechange();
+ self.onload();
+ }
+ };
+ }
+
+
+
+
+ /** public **/
+
+ open(method, url, async = true, username = "", password = "") {
+ this._xhr.method = method;
+
+ try {
+ this._xhr.uri = Services.io.newURI(url);
+ } catch (e) {
+ Components.utils.reportError(e);
+ throw new Error("HttpRequest: Invalid URL <"+url+">");
+ }
+ if (!async) throw new Error ("HttpRequest: Synchronous requests not implemented.");
+
+ this._xhr.username = username;
+ this._xhr.password = password;
+
+ this._xhr.readyState = 1;
+ this.onreadystatechange();
+
+ }
+
+ // must be called after open, before send
+ setContainerRealm(v) {
+ this._xhr.containerRealm = v;
+ }
+
+ // must be called after open, before send
+ clearContainerCache() {
+ this._xhr.containerReset = true;
+ }
+
+ send(data) {
+ //store the data, so we can rerun
+ this._xhr.data = data;
+ this._xhr.redirectFlags = null;
+
+ // The sandbox will have a loadingNode
+ let sandbox = getSandboxForOrigin(this._xhr.username, this._xhr.uri, this._xhr.containerRealm, this._xhr.containerReset);
+
+ // The XHR in the sandbox will have the correct loadInfo, which will allow us
+ // to use cookies and a CodebasePrincipal for us to use userContextIds and to
+ // contact nextcloud servers (error 503).
+ // We will not use the XHR or the sandbox itself.
+ let XHR = new sandbox.XMLHttpRequest();
+ XHR.open(this._xhr.method, this._xhr.uri.spec);
+
+ // Create the channel with the loadInfo from the sandboxed XHR
+ let channel = Services.io.newChannelFromURIWithLoadInfo(this._xhr.uri, XHR.channel.loadInfo);
+
+ /*
+ // as of TB67 newChannelFromURI needs to specify a loading node to have access to the cookie jars
+ // using the main window
+ // another option would be workers
+ let options = {};
+ let mainWindow = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let channel = Services.io.newChannelFromURI(
+ this._xhr.uri,
+ mainWindow.document,
+ Services.scriptSecurityManager.createContentPrincipal(this._xhr.uri, options),
+ null,
+ Components.interfaces.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
+ Components.interfaces.nsIContentPolicy.TYPE_OTHER);
+ */
+
+/*
+ // enforce anonymous access if requested (will not work with proxy, see MDN)
+ if (this._xhr.mozAnon) {
+ channel.loadFlag |= Components.interfaces.nsIRequest.LOAD_ANONYMOUS;
+ }
+
+ // set background request
+ if (this._xhr.mozBackgroundRequest) {
+ channel.loadFlag |= Components.interfaces.nsIRequest.LOAD_BACKGROUND;
+ }
+*/
+ this._xhr.httpchannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ this._xhr.httpchannel.loadFlags |= this._xhr.loadFlags;
+ this._xhr.httpchannel.notificationCallbacks = this.notificationCallbacks;
+
+ // Set default content type.
+ if (!this.hasRequestHeader("Content-Type")) {
+ this.setRequestHeader("Content-Type", "application/xml; charset=utf-8")
+ }
+
+ // Set default accept value.
+ if (!this.hasRequestHeader("Accept")) {
+ this.setRequestHeader("Accept", "*/*");
+ }
+
+ // Set non-standard header to request authorization (https://github.com/jobisoft/DAV-4-TbSync/issues/106)
+ if (this._xhr.username) {
+ this.setRequestHeader("X-EnforceAuthentication", "True");
+ }
+
+ // calculate length of request and add header
+ if (data) {
+ let textEncoder = new TextEncoder();
+ let encoded = textEncoder.encode(data);
+ this.setRequestHeader("Content-Length", encoded.length);
+ }
+
+ // mitigation for bug 669675
+ if (
+ bug669675.includes(this._xhr.uri.spec) &&
+ !this._xhr.mozAnon &&
+ !this.hasRequestHeader("Authorization") &&
+ this._xhr.username &&
+ this._xhr.password
+ ) {
+ this.setRequestHeader("Authorization", "Basic " + b64EncodeUnicode(this._xhr.username + ':' + this._xhr.password));
+ }
+
+ // add all headers to the channel
+ for (let header in this._xhr.headers) {
+ if (this._xhr.headers.hasOwnProperty(header)) {
+ this._xhr.httpchannel.setRequestHeader(header, this._xhr.headers[header], false);
+ }
+ }
+
+ // Will overwrite the Content-Type, so it must be called after the headers have been set.
+ prepHttpChannelUploadData(this._xhr.httpchannel, this._xhr.method, data, this.getRequestHeader("Content-Type"));
+
+ if (this._xhr.useStreamLoader) {
+ let loader = Components.classes["@mozilla.org/network/stream-loader;1"].createInstance(Components.interfaces.nsIStreamLoader);
+ loader.init(this.listener);
+ this.listener = loader;
+ }
+
+ this._startTimeout();
+ this._xhr.httpchannel.asyncOpen(this.listener, this._xhr.httpchannel);
+ }
+
+ get readyState() { return this._xhr.readyState; }
+ get responseURI() { return this._xhr.httpchannel.URI; }
+ get responseURL() { return this._xhr.httpchannel.URI.spec; }
+ get permanentlyRedirectedUrl() { return this._xhr.permanentlyRedirectedUrl; }
+ get responseText() { return this._xhr.responseText; }
+ get status() { return this._xhr.responseStatus; }
+ get statusText() { return this._xhr.responseStatusText; }
+ get channel() { return this._xhr.httpchannel; }
+ get loadFlags() { return this._xhr.loadFlags; }
+ get timeout() { return this._xhr.timeout; }
+ get mozBackgroundRequest() { return this._xhr.mozBackgroundRequest; }
+ get mozAnon() { return this._xhr.mozAnon; }
+
+ set loadFlags(v) { this._xhr.loadFlags = v; }
+ set timeout(v) { this._xhr.timeout = v; }
+ set mozBackgroundRequest(v) { this._xhr.mozBackgroundRequest = (v === true); }
+ set mozAnon(v) { this._xhr.mozAnon = (v === true); }
+
+
+ // case insensitive method to check for headers
+ hasRequestHeader(header) {
+ let lowHeaders = Object.keys(this._xhr.headers).map(x => x.toLowerCase());
+ return lowHeaders.includes(header.toLowerCase());
+ }
+
+ // if a header exists (case insensitive), it will be replaced (keeping the original capitalization)
+ setRequestHeader(header, value) {
+ let useHeader = header;
+ let lowHeader = header.toLowerCase();
+
+ for (let h in this._xhr.headers) {
+ if (this._xhr.headers.hasOwnProperty(h) && h.toLowerCase() == lowHeader) {
+ useHeader = h;
+ break;
+ }
+ }
+ this._xhr.headers[useHeader] = value;
+ }
+
+ // checks if a header (case insensitive) has been set by setRequestHeader - that does not mean it has been added to the channel!
+ getRequestHeader(header) {
+ let lowHeader = header.toLowerCase();
+
+ for (let h in this._xhr.headers) {
+ if (this._xhr.headers.hasOwnProperty(h) && h.toLowerCase() == lowHeader) {
+ return this._xhr.headers[h];
+ }
+ }
+ return null;
+ }
+
+ getResponseHeader(header) {
+ try {
+ return this._xhr.httpchannel.getResponseHeader(header);
+ } catch (e) {
+ if (e.code != Components.results.NS_ERROR_NOT_AVAILIBLE) {
+ // The header could possibly not be available, ignore that
+ // case but throw otherwise
+ throw e;
+ }
+ }
+ return null;
+ }
+
+ overrideMimeType(mime) {
+ this._xhr.overrideMimeType = mime;
+ }
+
+ abort() {
+ this._cancel(Components.results.NS_BINDING_ABORTED);
+ }
+
+ get responseAsBase64() { return this._xhr.responseAsBase64; }
+ set responseAsBase64(v) { this._xhr.responseAsBase64 = (v == true);}
+
+ /* not used */
+
+ get responseXML() { throw new Error("HttpRequest: responseXML not implemented"); }
+
+ get response() { throw new Error("HttpRequest: response not implemented"); }
+ set response(v) { throw new Error("HttpRequest: response not implemented"); }
+
+ get responseType() { throw new Error("HttpRequest: response not implemented"); }
+ set responseType(v) { throw new Error("HttpRequest: response not implemented"); }
+
+ get upload() { throw new Error("HttpRequest: upload not implemented"); }
+ set upload(v) { throw new Error("HttpRequest: upload not implemented"); }
+
+ get withCredentials() { throw new Error("HttpRequest: withCredentials not implemented"); }
+ set withCredentials(v) { throw new Error("HttpRequest: withCredentials not implemented"); }
+
+
+
+
+
+
+ /** private helper methods **/
+
+ _startTimeout() {
+ let that = this;
+
+ this._xhr.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ let event = {
+ notify: function(timer) {
+ that._cancel(Components.results.NS_ERROR_NET_TIMEOUT)
+ }
+ }
+ this._xhr.timer.initWithCallback(
+ event,
+ this._xhr.timeout,
+ Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ _cancel(error) {
+ if (this._xhr.httpchannel && error) {
+ this._xhr.httpchannel.cancel(error);
+ }
+ }
+}
+
+
+
+
+
+var HttpRequestPrompt = class {
+ constructor(username, password, promptCallback, realmCallback) {
+ this.mCounts = 0;
+ this.mUsername = username;
+ this.mPassword = password;
+ this.mPromptCallback = promptCallback;
+ this.mRealmCallback = realmCallback;
+ }
+
+ // boolean promptAuth(in nsIChannel aChannel,
+ // in uint32_t level,
+ // in nsIAuthInformation authInfo)
+ promptAuth (aChannel, aLevel, aAuthInfo) {
+ this.mRealmCallback(this.mUsername, aAuthInfo.realm, aChannel.URI.host);
+ if (this.mUsername && this.mPassword) {
+ console.log("Passing provided credentials for user <"+this.mUsername+"> to nsIHttpChannel.");
+ aAuthInfo.username = this.mUsername;
+ aAuthInfo.password = this.mPassword;
+ } else if (this.mUsername) {
+ console.log("Using passwordCallback callback to get password for user <"+this.mUsername+"> and realm <"+aAuthInfo.realm+"> @ host <"+aChannel.URI.host+">");
+ let password = this.mPromptCallback(this.mUsername, aAuthInfo.realm, aChannel.URI.host);
+ if (password) {
+ aAuthInfo.username = this.mUsername;
+ aAuthInfo.password = password;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+
+ // The provided password could be wrong, in whichcase
+ // we would be here more than once.
+ this.mCounts++
+ return (this.mCounts < 2);
+ }
+}
+
+
+
+
+function getSandboxForOrigin(username, uri, containerRealm = "default", containerReset = false) {
+ let options = {};
+ let origin = uri.scheme + "://" + uri.hostPort;
+
+ // Note:
+ // Disabled check for username to always use containers, even if no username is given.
+ // There was an issue with NC (in its container) and google being in the default container,
+ // causing NC to fail with 503. Putting google inside a container as well fixed it.
+ options.userContextId = getContainerIdForUser(containerRealm + "::" + username);
+ origin = options.userContextId + "@" + origin;
+ if (containerReset) {
+ resetContainerWithId(options.userContextId);
+ }
+
+ if (!sandboxes.hasOwnProperty(origin)) {
+ console.log("Creating sandbox for <"+origin+">");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(uri, options);
+ sandboxes[origin] = Components.utils.Sandbox(principal, {
+ wantXrays: true,
+ wantGlobalProperties: ["XMLHttpRequest"],
+ });
+ }
+
+ return sandboxes[origin];
+}
+
+function resetContainerWithId(id) {
+ Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: id });
+}
+
+function getContainerIdForUser(username) {
+ // Define the allowed range of container ids to be used
+ // TbSync is using 10000 - 19999
+ // Lightning is using 20000 - 29999
+ // Cardbook is using 30000 - 39999
+ let min = 10000;
+ let max = 19999;
+
+ //reset if adding an entry will exceed allowed range
+ if (containers.length > (max-min) && containers.indexOf(username) == -1) {
+ for (let i=0; i < containers.length; i++) {
+ resetContainerWithId(i + min);
+ }
+ containers = [];
+ }
+
+ let idx = containers.indexOf(username);
+ return (idx == -1) ? containers.push(username) - 1 + min : (idx + min);
+}
+
+// copied from cardbook
+function b64EncodeUnicode (aString) {
+ return btoa(encodeURIComponent(aString).replace(/%([0-9A-F]{2})/g, function(match, p1) {
+ return String.fromCharCode('0x' + p1);
+ }));
+}
+
+// copied from lightning
+function prepHttpChannelUploadData(aHttpChannel, aMethod, aUploadData, aContentType) {
+ if (aUploadData) {
+ aHttpChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ let stream;
+ if (aUploadData instanceof Components.interfaces.nsIInputStream) {
+ // Make sure the stream is reset
+ stream = aUploadData.QueryInterface(Components.interfaces.nsISeekableStream);
+ stream.seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, 0);
+ } else {
+ // Otherwise its something that should be a string, convert it.
+ let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ stream = converter.convertToInputStream(aUploadData.toString());
+ }
+
+ // If aContentType is empty, the protocol will assume that no content headers are to be
+ // added to the uploaded stream and that any required headers are already encoded in
+ // the stream. In the case of HTTP, if this parameter is non-empty, then its value will
+ // replace any existing Content-Type header on the HTTP request. In the case of FTP and
+ // FILE, this parameter is ignored.
+ aHttpChannel.setUploadStream(stream, aContentType, -1);
+ }
+
+ //must be set after setUploadStream
+ //https://developer.mozilla.org/en-US/docs/Mozilla/Creating_sandboxed_HTTP_connections
+ aHttpChannel.QueryInterface(Ci.nsIHttpChannel);
+ aHttpChannel.requestMethod = aMethod;
+}
+
+/**
+ * Convert a byte array to a string - copied from lightning
+ *
+ * @param {octet[]} aResult The bytes to convert
+ * @param {Boolean} responseAsBase64 Return a base64 encoded string
+ * @param {String} aCharset The character set of the bytes, defaults to utf-8
+ * @param {Boolean} aThrow If true, the function will raise an exception on error
+ * @returns {?String} The string result, or null on error
+ */
+function convertByteArray(aResult, responseAsBase64 = false, aCharset="utf-8", aThrow) {
+ if (responseAsBase64) {
+ var bin = '';
+ var bytes = Uint8Array.from(aResult);
+ var len = bytes.byteLength;
+ for (var i = 0; i < len; i++) {
+ bin += String.fromCharCode( bytes[ i ] );
+ }
+ return btoa( bin ); // if we ever need raw, return bin
+ } else {
+ try {
+ return new TextDecoder(aCharset).decode(Uint8Array.from(aResult));
+ } catch (e) {
+ if (aThrow) {
+ throw e;
+ }
+ }
+ }
+ return null;
+}
+
diff --git a/content/OverlayManager.jsm b/content/OverlayManager.jsm
new file mode 100644
index 0000000..a0c18d6
--- /dev/null
+++ b/content/OverlayManager.jsm
@@ -0,0 +1,514 @@
+/*
+ * 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 EXPORTED_SYMBOLS = ["OverlayManager"];
+
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function OverlayManager(extension, options = {}) {
+ this.registeredOverlays = {};
+ this.overlays = {};
+ this.stylesheets = {};
+ this.options = {verbose: 0};
+ this.extension = extension;
+
+ let userOptions = Object.keys(options);
+ for (let i=0; i < userOptions.length; i++) {
+ this.options[userOptions[i]] = options[userOptions[i]];
+ }
+
+
+
+ this.windowListener = {
+ that: this,
+ onOpenWindow: function(xulWindow) {
+ let window = xulWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);
+ this.that.injectAllOverlays(window);
+ },
+ onCloseWindow: function(xulWindow) { },
+ onWindowTitleChange: function(xulWindow, newTitle) { }
+ };
+
+
+
+
+ this.startObserving = function () {
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ //inject overlays for this window
+ this.injectAllOverlays(window);
+ }
+
+ Services.wm.addListener(this.windowListener);
+ };
+
+ this.stopObserving = function () {
+ Services.wm.removeListener(this.windowListener);
+
+ let windows = Services.wm.getEnumerator(null);
+ while (windows.hasMoreElements()) {
+ let window = windows.getNext();
+ //remove overlays (if any)
+ this.removeAllOverlays(window);
+ }
+ };
+
+ this.hasRegisteredOverlays = function (window) {
+ return this.registeredOverlays.hasOwnProperty(window.location.href);
+ };
+
+ this.registerOverlay = async function (dst, overlay) {
+ if (overlay.startsWith("chrome://")) {
+ let xul = null;
+ try {
+ xul = await this.readChromeFile(overlay);
+ } catch (e) {
+ console.log("Error reading file <"+overlay+"> : " + e);
+ return;
+ }
+ let rootNode = this.getDataFromXULString(xul);
+
+ if (rootNode) {
+ //get urls of stylesheets to load them
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ //we must replace, since we do not know, if it changed - could have been an update
+ //if (!this.stylesheets.hasOwnProperty(styleSheetUrls[i])) {
+ this.stylesheets[styleSheetUrls[i]] = await this.readChromeFile(styleSheetUrls[i]);
+ //}
+ }
+
+ if (!this.registeredOverlays[dst]) this.registeredOverlays[dst] = [];
+ if (!this.registeredOverlays[dst].includes(overlay)) this.registeredOverlays[dst].push(overlay);
+
+ this.overlays[overlay] = rootNode;
+ }
+ } else {
+ console.log("Only chrome:// URIs can be registered as overlays.");
+ }
+ };
+
+ this.getDataFromXULString = function (str) {
+ let data = null;
+ let xul = "";
+ if (str == "") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file is empty!");
+ return null;
+ }
+
+ let oParser = new DOMParser();
+ try {
+ xul = oParser.parseFromString(str, "application/xml");
+ } catch (e) {
+ //however, domparser does not throw an error, it returns an error document
+ //https://developer.mozilla.org/de/docs/Web/API/DOMParser
+ //just in case
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file could not be parsed correctly, something is wrong.\n" + str);
+ return null;
+ }
+
+ //check if xul is error document
+ if (xul.documentElement.nodeName == "parsererror") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file could not be parsed correctly, something is wrong.\n" + str);
+ return null;
+ }
+
+ if (xul.documentElement.nodeName != "overlay") {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A provided XUL file does not look like an overlay (root node is not overlay).\n" + str);
+ return null;
+ }
+
+ return xul;
+ };
+
+
+
+
+
+ this.injectAllOverlays = async function (window, _href = null) {
+ if (window.document.readyState != "complete") {
+ // Make sure the window load has completed.
+ await new Promise(resolve => {
+ window.addEventListener("load", resolve, { once: true });
+ });
+ }
+
+ let href = (_href === null) ? window.location.href : _href;
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] Injecting into new window: " + href);
+ let injectCount = 0;
+ for (let i=0; this.registeredOverlays[href] && i < this.registeredOverlays[href].length; i++) {
+ if (this.injectOverlay(window, this.registeredOverlays[href][i])) injectCount++;
+ }
+ if (injectCount > 0) {
+ // dispatch a custom event to indicate we finished loading the overlay
+ let event = new Event("DOMOverlayLoaded_" + this.extension.id);
+ window.document.dispatchEvent(event);
+ }
+ };
+
+ this.removeAllOverlays = function (window) {
+ if (!this.hasRegisteredOverlays(window))
+ return;
+
+ for (let i=0; i < this.registeredOverlays[window.location.href].length; i++) {
+ this.removeOverlay(window, this.registeredOverlays[window.location.href][i]);
+ }
+ };
+
+
+
+
+ this.injectOverlay = function (window, overlay) {
+ if (!window.hasOwnProperty("injectedOverlays")) window.injectedOverlays = [];
+
+ if (window.injectedOverlays.includes(overlay)) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] NOT Injecting: " + overlay);
+ return false;
+ }
+
+ let rootNode = this.overlays[overlay];
+
+ if (rootNode) {
+ let overlayNode = rootNode.documentElement;
+ if (overlayNode) {
+ //get and load scripts
+ let scripts = this.getScripts(rootNode, overlayNode);
+ for (let i=0; i < scripts.length; i++){
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Loading: " + scripts[i]);
+ try {
+ Services.scriptloader.loadSubScript(scripts[i], window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ let omscopename = overlayNode.hasAttribute("omscope") ? overlayNode.getAttribute("omscope") : null;
+ let omscope = omscopename ? window[omscopename] : window;
+
+ let inject = true;
+ if (omscope.hasOwnProperty("onBeforeInject")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onBeforeInject()");
+ try {
+ inject = omscope.onBeforeInject(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ if (inject) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] Injecting: " + overlay);
+ window.injectedOverlays.push(overlay);
+
+ //get urls of stylesheets to add preloaded files
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ let namespace = overlayNode.lookupNamespaceURI("html");
+ let element = window.document.createElementNS(namespace, "style");
+ element.id = styleSheetUrls[i];
+ element.textContent = this.stylesheets[styleSheetUrls[i]];
+ window.document.documentElement.appendChild(element);
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Stylesheet: " + styleSheetUrls[i]);
+ }
+
+ this.insertXulOverlay(window, overlayNode.children);
+ if (omscope.hasOwnProperty("onInject")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onInject()");
+ try {
+ omscope.onInject(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ // add to injectCounter
+ return true;
+ }
+ }
+ }
+
+ // nothing injected, do not add to inject counter
+ return false;
+ };
+
+ this.removeOverlay = function (window, overlay) {
+ if (!window.hasOwnProperty("injectedOverlays")) window.injectedOverlays = [];
+
+ if (!window.injectedOverlays.includes(overlay)) {
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] NOT Removing: " + overlay);
+ return;
+ }
+
+ if (this.options.verbose>2) Services.console.logStringMessage("[OverlayManager] Removing: " + overlay);
+ window.injectedOverlays = window.injectedOverlays.filter(e => (e != overlay));
+
+ let rootNode = this.overlays[overlay];
+ if (rootNode) {
+ let overlayNode = rootNode.documentElement;
+ if (overlayNode) {
+ let omscopename = overlayNode.hasAttribute("omscope") ? overlayNode.getAttribute("omscope") : null;
+ let omscope = omscopename ? window[omscopename] : window;
+
+ if (omscope.hasOwnProperty("onRemove")) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Executing " + (omscopename ? omscopename : "window") + ".onRemove()");
+ try {
+ omscope.onRemove(window);
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ this.removeXulOverlay(window, overlayNode.children);
+ }
+
+ //get urls of stylesheets to remove styte tag
+ let styleSheetUrls = this.getStyleSheetUrls(rootNode);
+ for (let i=0; i<styleSheetUrls.length; i++) {
+ let element = window.document.getElementById(styleSheetUrls[i]);
+ if (element) {
+ element.parentNode.removeChild(element);
+ }
+ }
+ }
+ };
+
+
+
+
+
+
+
+
+
+
+
+ this.getStyleSheetUrls = function (rootNode) {
+ let sheetsIterator = rootNode.evaluate("processing-instruction('xml-stylesheet')", rootNode, null, 0, null); //PathResult.ANY_TYPE = 0
+ let urls = [];
+
+ let sheet;
+ while (sheet = sheetsIterator.iterateNext()) { //returns object XMLStylesheetProcessingInstruction]
+ let attr=sheet.data.split(" ");
+ for (let a=0; a < attr.length; a++) {
+ if (attr[a].startsWith("href=")) urls.push(attr[a].substring(6,attr[a].length-1));
+ }
+ }
+ return urls;
+ };
+
+ this.getScripts = function(rootNode, overlayNode) {
+ let nodeIterator = rootNode.evaluate("./script", overlayNode, null, 0, null); //PathResult.ANY_TYPE = 0
+ let scripts = [];
+
+ let node;
+ while (node = nodeIterator.iterateNext()) {
+ if (node.hasAttribute("src") && node.hasAttribute("type") && node.getAttribute("type").toLowerCase().includes("javascript")) {
+ scripts.push(node.getAttribute("src"));
+ }
+ }
+ return scripts;
+ };
+
+
+
+
+
+
+
+
+
+
+ this.createXulElement = function (window, node, forcedNodeName = null) {
+ //check for namespace
+ let typedef = forcedNodeName ? forcedNodeName.split(":") : node.nodeName.split(":");
+ if (typedef.length == 2) typedef[0] = node.lookupNamespaceURI(typedef[0]);
+
+ let CE = {}
+ if (node.attributes && node.attributes.getNamedItem("is")) {
+ for (let i=0; i <node.attributes.length; i++) {
+ if (node.attributes[i].name == "is") {
+ CE = { "is" : node.attributes[i].value };
+ break;
+ }
+ }
+ }
+
+ let element = (typedef.length==2) ? window.document.createElementNS(typedef[0], typedef[1]) : window.document.createXULElement(typedef[0], CE);
+ if (node.attributes) {
+ for (let i=0; i <node.attributes.length; i++) {
+ element.setAttribute(node.attributes[i].name, node.attributes[i].value);
+ }
+ }
+
+ //add text child nodes as textContent
+ if (node.hasChildNodes) {
+ let textContent = "";
+ for (let child of node.childNodes) {
+ if (child.nodeType == "3") {
+ textContent += child.nodeValue;
+ }
+ }
+ if (textContent) element.textContent = textContent
+ }
+
+ return element;
+ };
+
+ this.insertXulOverlay = function (window, nodes, parentElement = null) {
+ /*
+ The passed nodes value could be an entire window.document in a single node (type 9) or a
+ single element node (type 1) as returned by getElementById. It could however also
+ be an array of nodes as returned by getElementsByTagName or a nodeList as returned
+ by childNodes. In that case node.length is defined.
+ */
+ let nodeList = [];
+ if (nodes.length === undefined) nodeList.push(nodes);
+ else nodeList = nodes;
+
+ // nodelist contains all childs
+ for (let node of nodeList) {
+ let element = null;
+ let hookMode = null;
+ let hookName = null;
+ let hookElement = null;
+
+ if (node.nodeName == "script" && node.hasAttribute("src")) {
+ //skip, since they are handled by getScripts()
+ } else if (node.nodeType == 1) {
+
+ if (!parentElement) { //misleading: if it does not have a parentElement, it is a top level element
+ //Adding top level elements without id is not allowed, because we need to be able to remove them!
+ if (!node.hasAttribute("id")) {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: A top level <" + node.nodeName+ "> element does not have an ID. Skipped");
+ continue;
+ }
+
+ //check for inline script tags
+ if (node.nodeName == "script") {
+ let element = this.createXulElement(window, node, "html:script"); //force as html:script
+ window.document.documentElement.appendChild(element);
+ continue;
+ }
+
+ //check for inline style
+ if (node.nodeName == "style") {
+ let element = this.createXulElement(window, node, "html:style"); //force as html:style
+ window.document.documentElement.appendChild(element);
+ continue;
+ }
+
+ if (node.hasAttribute("appendto")) hookMode = "appendto";
+ if (node.hasAttribute("insertbefore")) hookMode ="insertbefore";
+ if (node.hasAttribute("insertafter")) hookMode = "insertafter";
+
+ if (hookMode) {
+ hookName = node.getAttribute(hookMode);
+ hookElement = window.document.getElementById(hookName);
+
+ if (!hookElement) {
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: The hook element <"+hookName+"> of top level overlay element <"+ node.nodeName+"> does not exist. Skipped");
+ continue;
+ }
+ } else {
+ hookMode = "appendto";
+ hookName = "ROOT";
+ hookElement = window.document.documentElement;
+ }
+ }
+
+ element = this.createXulElement(window, node);
+ if (node.hasChildNodes) this.insertXulOverlay(window, node.children, element);
+
+ if (parentElement) {
+ // this is a child level XUL element which needs to be added to to its parent
+ parentElement.appendChild(element);
+ } else {
+ // this is a toplevel element, which needs to be added at insertafter or insertbefore
+ switch (hookMode) {
+ case "appendto":
+ hookElement.appendChild(element);
+ break;
+ case "insertbefore":
+ hookElement.parentNode.insertBefore(element, hookElement);
+ break;
+ case "insertafter":
+ hookElement.parentNode.insertBefore(element, hookElement.nextSibling);
+ break;
+ default:
+ if (this.options.verbose>1) Services.console.logStringMessage("[OverlayManager] BAD XUL: Top level overlay element <"+ node.nodeName+"> uses unknown hook type <"+hookMode+">. Skipped.");
+ continue;
+ }
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Adding <"+element.id+"> ("+element.tagName+") " + hookMode + " <" + hookName + ">");
+ }
+ }
+ }
+ };
+
+ this.removeXulOverlay = function (window, nodes, parentElement = null) {
+ //only scan toplevel elements and remove them
+ let nodeList = [];
+ if (nodes.length === undefined) nodeList.push(nodes);
+ else nodeList = nodes;
+
+ // nodelist contains all childs
+ for (let node of nodeList) {
+ let element = null;
+ switch(node.nodeType) {
+ case 1:
+ if (node.hasAttribute("id")) {
+ let element = window.document.getElementById(node.getAttribute("id"));
+ if (element) {
+ element.parentNode.removeChild(element);
+ }
+ }
+ break;
+ }
+ }
+ };
+
+
+
+
+
+
+
+
+
+
+ //read file from within the XPI package
+ this.readChromeFile = function (aURL) {
+ if (this.options.verbose>3) Services.console.logStringMessage("[OverlayManager] Reading file: " + aURL);
+ return new Promise((resolve, reject) => {
+ let uri = Services.io.newURI(aURL);
+ let channel = Services.io.newChannelFromURI(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ NetUtil.asyncFetch(channel, (inputStream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(status);
+ return;
+ }
+
+ try {
+ let data = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+ resolve(data);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ });
+ };
+
+}
diff --git a/content/api/BootstrapLoader/CHANGELOG.md b/content/api/BootstrapLoader/CHANGELOG.md
new file mode 100644
index 0000000..5006ecf
--- /dev/null
+++ b/content/api/BootstrapLoader/CHANGELOG.md
@@ -0,0 +1,75 @@
+Version: 1.21
+-------------
+- Explicitly set hasAddonManagerEventListeners flag to false on uninstall
+
+Version: 1.20
+-------------
+- hard fork BootstrapLoader v1.19 implementation and continue to serve it for
+ Thunderbird 111 and older
+- BootstrapLoader v1.20 has removed a lot of unnecessary code used for backward
+ compatibility
+
+Version: 1.19
+-------------
+- fix race condition which could prevent the AOM tab to be monkey patched correctly
+
+Version: 1.18
+-------------
+- be precise on which revision the wrench symbol should be displayed, instead of
+ the options button
+
+Version: 1.17
+-------------
+- fix "ownerDoc.getElementById() is undefined" bug
+
+Version: 1.16
+-------------
+- fix "tab.browser is undefined" bug
+
+Version 1.15
+------------
+- clear cache only if add-on is uninstalled/updated, not on app shutdown
+
+Version 1.14
+------------
+- fix for TB90 ("view-loaded" event) and TB78.10 (wrench icon for options)
+
+Version 1.13
+------------
+- removed notifyTools and move it into its own NotifyTools API
+
+Version 1.12
+------------
+- add support for notifyExperiment and onNotifyBackground
+
+Version 1.11
+------------
+- add openOptionsDialog()
+
+Version 1.10
+------------
+- fix for 68
+
+Version 1.7
+-----------
+- fix for beta 87
+
+Version 1.6
+-----------
+- add support for options button/menu in add-on manager and fix 68 double menu entry
+
+Version 1.5
+-----------
+- fix for e10s
+
+Version 1.4
+-----------
+- add registerOptionsPage
+
+Version 1.3
+-----------
+- flush cache
+
+Version 1.2
+-----------
+- add support for resource urls
diff --git a/content/api/BootstrapLoader/README.md b/content/api/BootstrapLoader/README.md
new file mode 100644
index 0000000..7e8fe2a
--- /dev/null
+++ b/content/api/BootstrapLoader/README.md
@@ -0,0 +1 @@
+Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-BootstrapLoader-API-to-convert-a-Legacy-Bootstrap-WebExtension-into-a-MailExtension-for-Thunderbird-78).
diff --git a/content/api/BootstrapLoader/implementation.js b/content/api/BootstrapLoader/implementation.js
new file mode 100644
index 0000000..03e6b76
--- /dev/null
+++ b/content/api/BootstrapLoader/implementation.js
@@ -0,0 +1,917 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.21
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * 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/.
+ */
+
+// Get various parts of the WebExtension framework that we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function getThunderbirdVersion() {
+ let parts = Services.appinfo.version.split(".");
+ return {
+ major: parseInt(parts[0]),
+ minor: parseInt(parts[1]),
+ revision: parts.length > 2 ? parseInt(parts[2]) : 0,
+ }
+}
+
+function getMessenger(context) {
+ let apis = ["storage", "runtime", "extension", "i18n"];
+
+ function getStorage() {
+ let localstorage = null;
+ try {
+ localstorage = context.apiCan.findAPIPath("storage");
+ localstorage.local.get = (...args) =>
+ localstorage.local.callMethodInParentProcess("get", args);
+ localstorage.local.set = (...args) =>
+ localstorage.local.callMethodInParentProcess("set", args);
+ localstorage.local.remove = (...args) =>
+ localstorage.local.callMethodInParentProcess("remove", args);
+ localstorage.local.clear = (...args) =>
+ localstorage.local.callMethodInParentProcess("clear", args);
+ } catch (e) {
+ console.info("Storage permission is missing");
+ }
+ return localstorage;
+ }
+
+ let messenger = {};
+ for (let api of apis) {
+ switch (api) {
+ case "storage":
+ XPCOMUtils.defineLazyGetter(messenger, "storage", () =>
+ getStorage()
+ );
+ break;
+
+ default:
+ XPCOMUtils.defineLazyGetter(messenger, api, () =>
+ context.apiCan.findAPIPath(api)
+ );
+ }
+ }
+ return messenger;
+}
+
+var BootstrapLoader_102 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc;
+
+ // 78,86, and 87+ need special handholding. *Yeah*.
+ if (getThunderbirdVersion().major < 86) {
+ let ownerDoc = e.document || e.target.ownerDocument;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else if (getThunderbirdVersion().major < 87) {
+ let ownerDoc = e.document || e.target;
+ doc = ownerDoc.getElementById("html-view-browser").contentDocument;
+ } else {
+ doc = e.document || e.target;
+ }
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Add pref entry to 68
+ add68PrefsEntry(event) {
+ let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
+
+ // Get the best size of the icon (16px or bigger)
+ let iconSizes = this.extension.manifest.icons
+ ? Object.keys(this.extension.manifest.icons)
+ : [];
+ iconSizes.sort((a, b) => a - b);
+ let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
+ let icon = bestSize ? this.extension.manifest.icons[bestSize] : "";
+
+ let name = this.extension.manifest.name;
+ let entry = icon
+ ? event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem class="menuitem-iconic" id="${id}" image="${icon}" label="${name}" />`)
+ : event.target.ownerGlobal.MozXULElement.parseXULToFragment(
+ `<menuitem id="${id}" label="${name}" />`);
+
+ event.target.appendChild(entry);
+ let noPrefsElem = event.target.querySelector('[disabled="true"]');
+ // using collapse could be undone by core, so we use display none
+ // noPrefsElem.setAttribute("collapsed", "true");
+ noPrefsElem.style.display = "none";
+ event.target.ownerGlobal.document.getElementById(id).addEventListener("command", this);
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ // 68 add-on options menu showing
+ case "popupshowing": {
+ this.add68PrefsEntry(e);
+ }
+ break;
+
+ // 78/88 add-on options menu/button click
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // 68 add-on options menu command
+ case "command": {
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ e.target.ownerGlobal.openDialog(this.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ }
+ break;
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ let optionsMenu =
+ (getThunderbirdVersion().major > 78 && getThunderbirdVersion().major < 88) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor < 10) ||
+ (getThunderbirdVersion().major == 78 && getThunderbirdVersion().minor == 10 && getThunderbirdVersion().revision < 2);
+ if (optionsMenu) {
+ // Options menu in 78.0-78.10 and 79-87
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (card.addon.isActive && !addonOptionsLegacyEntry) {
+ let addonOptionsEntry = card.querySelector("addon-options panel-list panel-item[action='preferences']");
+ addonOptionsLegacyEntry = card.ownerDocument.createElement("panel-item");
+ addonOptionsLegacyEntry.setAttribute("data-l10n-id", "preferences-addon-button");
+ addonOptionsLegacyEntry.classList.add("extension-options-legacy");
+ addonOptionsEntry.parentNode.insertBefore(
+ addonOptionsLegacyEntry,
+ addonOptionsEntry
+ );
+ card.querySelector(".extension-options-legacy").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsLegacyEntry) {
+ addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Add-on button in 88
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (card.addon.isActive && !addonOptionsButton) {
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("extension-options-button2");
+ addonOptionsButton.style["min-width"] = "auto";
+ addonOptionsButton.style["min-height"] = "auto";
+ addonOptionsButton.style["width"] = "24px";
+ addonOptionsButton.style["height"] = "24px";
+ addonOptionsButton.style["margin"] = "0";
+ addonOptionsButton.style["margin-inline-start"] = "8px";
+ addonOptionsButton.style["-moz-context-properties"] = "fill";
+ addonOptionsButton.style["fill"] = "currentColor";
+ addonOptionsButton.style["background-image"] = "url('chrome://messenger/skin/icons/developer.svg')";
+ addonOptionsButton.style["background-repeat"] = "no-repeat";
+ addonOptionsButton.style["background-position"] = "center center";
+ addonOptionsButton.style["padding"] = "1px";
+ addonOptionsButton.style["display"] = "flex";
+ addonOptionsButton.style["justify-content"] = "flex-end";
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card.querySelector(".extension-options-button2").addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+// Removed all extra code for backward compatibility for better maintainability.
+var BootstrapLoader_115 = class extends ExtensionCommon.ExtensionAPI {
+ getCards(e) {
+ // This gets triggered by real events but also manually by providing the outer window.
+ // The event is attached to the outer browser, get the inner one.
+ let doc = e.document || e.target;
+ return doc.querySelectorAll("addon-card");
+ }
+
+ // Event handler for the addon manager, to update the state of the options button.
+ handleEvent(e) {
+ switch (e.type) {
+ case "click": {
+ e.preventDefault();
+ e.stopPropagation();
+ let BL = {}
+ BL.extension = this.extension;
+ BL.messenger = getMessenger(this.context);
+ let w = Services.wm.getMostRecentWindow("mail:3pane");
+ w.openDialog(
+ this.pathToOptionsPage,
+ "AddonOptions",
+ "chrome,resizable,centerscreen",
+ BL
+ );
+ }
+ break;
+
+
+ // update, ViewChanged and manual call for add-on manager options overlay
+ default: {
+ let cards = this.getCards(e);
+ for (let card of cards) {
+ // Setup either the options entry in the menu or the button
+ if (card.addon.id == this.extension.id) {
+ // Add-on button
+ let addonOptionsButton = card.querySelector(
+ ".windowlistener-options-button"
+ );
+ if (card.addon.isActive && !addonOptionsButton) {
+ let origAddonOptionsButton = card.querySelector(".extension-options-button")
+ origAddonOptionsButton.setAttribute("hidden", "true");
+
+ addonOptionsButton = card.ownerDocument.createElement("button");
+ addonOptionsButton.classList.add("windowlistener-options-button");
+ addonOptionsButton.classList.add("extension-options-button");
+ card.optionsButton.parentNode.insertBefore(
+ addonOptionsButton,
+ card.optionsButton
+ );
+ card
+ .querySelector(".windowlistener-options-button")
+ .addEventListener("click", this);
+ } else if (!card.addon.isActive && addonOptionsButton) {
+ addonOptionsButton.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Some tab/add-on-manager related functions
+ getTabMail(window) {
+ return window.document.getElementById("tabmail");
+ }
+
+ // returns the outer browser, not the nested browser of the add-on manager
+ // events must be attached to the outer browser
+ getAddonManagerFromTab(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let win = tab.browser.contentWindow;
+ if (win && win.location.href == "about:addons") {
+ return win;
+ }
+ }
+ }
+
+ getAddonManagerFromWindow(window) {
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+
+ async getAddonManagerFromWindowWaitForLoad(window) {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+
+ let tabMail = this.getTabMail(window);
+ for (let tab of tabMail.tabInfo) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ let managerWindow = this.getAddonManagerFromTab(tab);
+ if (managerWindow) {
+ return managerWindow;
+ }
+ }
+ }
+ }
+
+ setupAddonManager(managerWindow, forceLoad = false) {
+ if (!managerWindow) {
+ return;
+ }
+ if (!this.pathToOptionsPage) {
+ return;
+ }
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ return;
+ }
+
+ managerWindow.document.addEventListener("ViewChanged", this);
+ managerWindow.document.addEventListener("update", this);
+ managerWindow.document.addEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID] = {};
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = true;
+ if (forceLoad) {
+ this.handleEvent(managerWindow);
+ }
+ }
+
+ getAPI(context) {
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonPrefs_id = "addonPrefs";
+
+
+ this.pathToBootstrapScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = getMessenger(this.context);
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ // TabMonitor to detect opening of tabs, to setup the options button in the add-on manager.
+ this.tabMonitor = {
+ onTabTitleChanged(tab) { },
+ onTabClosing(tab) { },
+ onTabPersist(tab) { },
+ onTabRestored(tab) { },
+ onTabSwitched(aNewTab, aOldTab) { },
+ async onTabOpened(tab) {
+ if (tab.browser && tab.mode.name == "contentTab") {
+ let { setTimeout } = Services.wm.getMostRecentWindow("mail:3pane");
+ // Instead of registering a load observer, wait until its loaded. Not nice,
+ // but gets aroud a lot of edge cases.
+ while (!tab.pageLoaded) {
+ await new Promise(r => setTimeout(r, 150));
+ }
+ self.setupAddonManager(self.getAddonManagerFromTab(tab));
+ }
+ },
+ };
+
+ return {
+ BootstrapLoader: {
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ openOptionsDialog(windowId) {
+ let window = context.extension.windowManager.get(windowId, context).window
+ let BL = {}
+ BL.extension = self.extension;
+ BL.messenger = getMessenger(self.context);
+ window.openDialog(self.pathToOptionsPage, "AddonOptions", "chrome,resizable,centerscreen", BL);
+ },
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function (aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Register window listener for main TB window
+ if (self.pathToOptionsPage) {
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ chromeURLs: [
+ "chrome://messenger/content/messenger.xul",
+ "chrome://messenger/content/messenger.xhtml",
+ ],
+ async onLoadWindow(window) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ element_addonPrefs.addEventListener("popupshowing", self);
+ } else {
+ // Add a tabmonitor, to be able to setup the options button/menu in the add-on manager.
+ self.getTabMail(window).registerTabMonitor(self.tabMonitor);
+ window[self.uniqueRandomID] = {};
+ window[self.uniqueRandomID].hasTabMonitor = true;
+ // Setup the options button/menu in the add-on manager, if it is already open.
+ let managerWindow = await self.getAddonManagerFromWindowWaitForLoad(window);
+ self.setupAddonManager(managerWindow, true);
+ }
+ },
+
+ onUnloadWindow(window) {
+ }
+ });
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return; // the application gets unloaded anyway
+ }
+
+ //remove our entry in the add-on options menu
+ if (this.pathToOptionsPage) {
+ for (let window of Services.wm.getEnumerator("mail:3pane")) {
+ if (getThunderbirdVersion().major < 78) {
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ element_addonPrefs.removeEventListener("popupshowing", this);
+ // Remove our entry.
+ let entry = window.document.getElementById(this.menu_addonPrefs_id + "_" + this.uniqueRandomID);
+ if (entry) entry.remove();
+ // Do we have to unhide the noPrefsElement?
+ if (element_addonPrefs.children.length == 1) {
+ let noPrefsElem = element_addonPrefs.querySelector('[disabled="true"]');
+ noPrefsElem.style.display = "inline";
+ }
+ } else {
+ // Remove event listener for addon manager view changes
+ let managerWindow = this.getAddonManagerFromWindow(window);
+ if (
+ managerWindow &&
+ managerWindow[this.uniqueRandomID] &&
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners
+ ) {
+ managerWindow.document.removeEventListener("ViewChanged", this);
+ managerWindow.document.removeEventListener("update", this);
+ managerWindow.document.removeEventListener("view-loaded", this);
+ managerWindow[this.uniqueRandomID].hasAddonManagerEventListeners = false;
+
+ let cards = this.getCards(managerWindow);
+ if (getThunderbirdVersion().major < 88) {
+ // Remove options menu in 78-87
+ for (let card of cards) {
+ let addonOptionsLegacyEntry = card.querySelector(".extension-options-legacy");
+ if (addonOptionsLegacyEntry) addonOptionsLegacyEntry.remove();
+ }
+ } else {
+ // Remove options button in 88
+ for (let card of cards) {
+ if (card.addon.id == this.extension.id) {
+ let addonOptionsButton = card.querySelector(".extension-options-button2");
+ if (addonOptionsButton) addonOptionsButton.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ // Remove tabmonitor
+ if (window[this.uniqueRandomID].hasTabMonitor) {
+ this.getTabMail(window).unregisterTabMonitor(this.tabMonitor);
+ window[this.uniqueRandomID].hasTabMonitor = false;
+ }
+
+ }
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
+
+var BootstrapLoader = getThunderbirdVersion().major < 111
+ ? BootstrapLoader_102
+ : BootstrapLoader_115;
diff --git a/content/api/BootstrapLoader/schema.json b/content/api/BootstrapLoader/schema.json
new file mode 100644
index 0000000..fe48fb6
--- /dev/null
+++ b/content/api/BootstrapLoader/schema.json
@@ -0,0 +1,61 @@
+[
+ {
+ "namespace": "BootstrapLoader",
+ "functions": [
+ {
+ "name": "registerOptionsPage",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
+ }
+ ]
+ },
+ {
+ "name": "openOptionsDialog",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowId",
+ "type": "integer",
+ "description": "Id of the window the dialog should be opened from."
+ }
+ ]
+ },
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "async": true,
+ "parameters": [
+ {
+ "name": "chromeData",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "description": "Array of ChromeData Arrays."
+ }
+ ]
+ },
+ {
+ "name": "registerBootstrapScript",
+ "type": "function",
+ "description": "Register a bootstrap.js style script",
+ "async": true,
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Either the chrome:// path to the script or its relative location from the root of the extension,"
+ }
+ ]
+ }
+ ]
+ }
+] \ No newline at end of file
diff --git a/content/manager/accountManager.js b/content/manager/accountManager.js
new file mode 100644
index 0000000..d442222
--- /dev/null
+++ b/content/manager/accountManager.js
@@ -0,0 +1,140 @@
+/*
+ * 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncAccountManager = {
+
+ onloadoptions: function () {
+ window.close();
+ },
+
+ onunloadoptions: function () {
+ TbSync.manager.openManagerWindow(0);
+ },
+
+ onload: function () {
+ TbSync.AccountManagerTabs = ["accounts.xhtml", "catman.xhtml", "supporter.xhtml", "help.xhtml"];
+ tbSyncAccountManager.selectTab(0);
+ },
+
+ onunload: function () {
+ TbSync.manager.prefWindowObj = null;
+ },
+
+ selectTab: function (t) {
+ const LOAD_FLAGS_NONE = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ //set active tab (css selector for background color)
+ for (let i=0; i<TbSync.AccountManagerTabs.length; i++) {
+ if (i==t) document.getElementById("tbSyncAccountManager.t" + i).setAttribute("active","true");
+ else document.getElementById("tbSyncAccountManager.t" + i).setAttribute("active","false");
+ }
+ TbSync.manager.prefWindowObj.document.getElementById("tbSyncAccountManager.installProvider").hidden=true;
+
+ //load XUL
+ document.getElementById("tbSyncAccountManager.contentWindow").setAttribute("src", "chrome://tbsync/content/manager/"+TbSync.AccountManagerTabs[t]);
+ },
+
+
+
+ //help tab
+ getLogPref: function() {
+ let log = document.getElementById("tbSyncAccountManager.logLevel");
+ log.value = Math.min(3, TbSync.prefs.getIntPref("log.userdatalevel"));
+ },
+
+ toggleLogPref: function() {
+ let log = document.getElementById("tbSyncAccountManager.logLevel");
+ TbSync.prefs.setIntPref("log.userdatalevel", log.value);
+ },
+
+ initSupportWizard: function() {
+ document.getElementById("SupportWizard").getButton("finish").disabled = true;
+
+ let menu = document.getElementById("tbsync.supportwizard.faultycomponent");
+
+ let providers = Object.keys(TbSync.providers.loadedProviders);
+ for (let i=0; i < providers.length; i++) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("value", providers[i]);
+ item.setAttribute("label", TbSync.getString("supportwizard.provider::" + TbSync.providers[providers[i]].Base.getProviderName()));
+ menu.appendChild(item);
+ }
+
+ document.getElementById("tbsync.supportwizard.faultycomponent.menulist").addEventListener("select", tbSyncAccountManager.checkSupportWizard);
+ document.getElementById("tbsync.supportwizard.description").addEventListener("input", tbSyncAccountManager.checkSupportWizard);
+ document.addEventListener("wizardfinish", tbSyncAccountManager.prepareBugReport);
+
+ // bug https://bugzilla.mozilla.org/show_bug.cgi?id=1618252
+ document.getElementById('SupportWizard')._adjustWizardHeader();
+ },
+
+ checkSupportWizard: function() {
+ let provider = document.getElementById("tbsync.supportwizard.faultycomponent").parentNode.value;
+ let subject = document.getElementById("tbsync.supportwizard.summary").value;
+ let description = document.getElementById("tbsync.supportwizard.description").value;
+
+ //just check and update button status
+ document.getElementById("SupportWizard").getButton("finish").disabled = (provider == "" || subject == "" || description== "");
+ },
+
+ prepareBugReport: function(event) {
+ let provider = document.getElementById("tbsync.supportwizard.faultycomponent").parentNode.value;
+ let subject = document.getElementById("tbsync.supportwizard.summary").value;
+ let description = document.getElementById("tbsync.supportwizard.description").value;
+
+ if (provider == "" || subject == "" || description== "") {
+ event.preventDefault();
+ return;
+ }
+
+ //special if core is selected, which is not a provider
+ let email = (TbSync.providers.loadedProviders.hasOwnProperty(provider)) ? TbSync.providers[provider].Base.getMaintainerEmail() : "john.bieling@gmx.de";
+ let version = (TbSync.providers.loadedProviders.hasOwnProperty(provider)) ? " " + TbSync.providers.loadedProviders[provider].version : "";
+ TbSync.manager.createBugReport(email, "[" + provider.toUpperCase() + version + "] " + subject, description);
+ },
+
+
+
+ //community tab
+ initCommunity: function() {
+ let listOfContributors = document.getElementById("listOfContributors");
+ let sponsors = {};
+
+ let providers = Object.keys(TbSync.providers.loadedProviders);
+ for (let i=0; i < providers.length; i++) {
+ let provider = providers[i];
+ let template = listOfContributors.firstElementChild.cloneNode(true);
+ template.setAttribute("provider", provider);
+ template.children[0].setAttribute("src", TbSync.providers[provider].Base.getProviderIcon(48));
+ template.children[1].children[0].textContent = TbSync.providers[provider].Base.getProviderName();
+ listOfContributors.appendChild(template);
+ Object.assign(sponsors, TbSync.providers[provider].Base.getSponsors());
+ }
+ listOfContributors.removeChild(listOfContributors.firstElementChild);
+
+ let listOfSponsors = document.getElementById("listOfSponsors");
+ let sponsorlist = Object.keys(sponsors);
+ sponsorlist.sort();
+ for (let i=0; i < sponsorlist.length; i++) {
+ let sponsor = sponsors[sponsorlist[i]];
+ let template = listOfSponsors.firstElementChild.cloneNode(true);
+ if (sponsor.link) template.setAttribute("link", sponsor.link);
+ if (sponsor.icon) template.children[0].setAttribute("src", sponsor.icon);
+ template.children[1].children[0].textContent = sponsor.name;
+ template.children[1].children[1].textContent = sponsor.description;
+ listOfSponsors.appendChild(template);
+ listOfSponsors.appendChild(template);
+ }
+ listOfSponsors.removeChild(listOfSponsors.firstElementChild);
+ }
+};
diff --git a/content/manager/accountManager.xhtml b/content/manager/accountManager.xhtml
new file mode 100644
index 0000000..d136240
--- /dev/null
+++ b/content/manager/accountManager.xhtml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="__TBSYNCMSG_manager.title__"
+ onload="tbSyncAccountManager.onload();"
+ onunload="tbSyncAccountManager.onunload();"
+ width="760" height="620" >
+
+ <vbox id="manager" flex="1">
+ <hbox id="tbtoolbar">
+ <vbox id="tbSyncAccountManager.t0" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="tbSyncAccountManager.selectTab(0)"><hbox flex="1" pack="center" style="min-width:60px"><image src="chrome://tbsync/content/skin/settings32.png" style="width: 32px; height: 32px" /></hbox><hbox flex="1" pack="center"><label value="__TBSYNCMSG_manager.accountsettings__" /></hbox></vbox>
+ <vbox id="tbSyncAccountManager.t1" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="tbSyncAccountManager.selectTab(1)"><hbox flex="1" pack="center" style="min-width:60px"><image src="chrome://tbsync/content/skin/catman32.png" style="width: 32px; height: 32px" /></hbox><hbox flex="1" pack="center"><label value="Category Manager" /></hbox></vbox>
+ <vbox id="tbSyncAccountManager.t2" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="tbSyncAccountManager.selectTab(2)"><hbox flex="1" pack="center" style="min-width:60px"><image src="chrome://tbsync/content/skin/group32.png" style="width: 32px; height: 32px" /></hbox><hbox flex="1" pack="center"><label value="__TBSYNCMSG_manager.community__" /></hbox></vbox>
+ <vbox id="tbSyncAccountManager.t3" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="tbSyncAccountManager.selectTab(3)"><hbox flex="1" pack="center" style="min-width:60px"><image src="chrome://tbsync/content/skin/help32.png" style="width: 32px; height: 32px" /></hbox><hbox flex="1" pack="center"><label value="__TBSYNCMSG_manager.help__" /></hbox></vbox>
+ <vbox id="tbSyncAccountManager.installProvider" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" hidden="true"><hbox flex="1" pack="center" style="min-width:60px"><image src="chrome://tbsync/content/skin/provider32.png" style="width: 32px; height: 32px" /></hbox><hbox flex="1" pack="center"><label value="__TBSYNCMSG_manager.provider__" /></hbox></vbox>
+ </hbox>
+ <browser id="tbSyncAccountManager.contentWindow" type="chrome" src="" disablehistory="true" flex="1"/>
+ </vbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/accounts.js b/content/manager/accounts.js
new file mode 100644
index 0000000..45774b1
--- /dev/null
+++ b/content/manager/accounts.js
@@ -0,0 +1,498 @@
+/*
+ * 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncAccounts = {
+
+ selectedAccount: null,
+
+ onload: function () {
+ //scan accounts, update list and select first entry (because no id is passed to updateAccountList)
+ //the onSelect event of the List will load the selected account
+ //also update/init add menu
+ this.updateAvailableProvider();
+
+ Services.obs.addObserver(tbSyncAccounts.updateProviderListObserver, "tbsync.observer.manager.updateProviderList", false);
+ Services.obs.addObserver(tbSyncAccounts.updateAccountsListObserver, "tbsync.observer.manager.updateAccountsList", false);
+ Services.obs.addObserver(tbSyncAccounts.updateAccountSyncStateObserver, "tbsync.observer.manager.updateSyncstate", false);
+ Services.obs.addObserver(tbSyncAccounts.updateAccountNameObserver, "tbsync.observer.manager.updateAccountName", false);
+ Services.obs.addObserver(tbSyncAccounts.toggleEnableStateObserver, "tbsync.observer.manager.toggleEnableState", false);
+ },
+
+ onunload: function () {
+ Services.obs.removeObserver(tbSyncAccounts.updateProviderListObserver, "tbsync.observer.manager.updateProviderList");
+ Services.obs.removeObserver(tbSyncAccounts.updateAccountsListObserver, "tbsync.observer.manager.updateAccountsList");
+ Services.obs.removeObserver(tbSyncAccounts.updateAccountSyncStateObserver, "tbsync.observer.manager.updateSyncstate");
+ Services.obs.removeObserver(tbSyncAccounts.updateAccountNameObserver, "tbsync.observer.manager.updateAccountName");
+ Services.obs.removeObserver(tbSyncAccounts.toggleEnableStateObserver, "tbsync.observer.manager.toggleEnableState");
+ },
+
+ hasInstalledProvider: function (accountID) {
+ let provider = TbSync.db.getAccountProperty(accountID, "provider");
+ return TbSync.providers.loadedProviders.hasOwnProperty(provider);
+ },
+
+ updateDropdown: function (selector) {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+ let selectedAccount = null;
+ let selectedAccountName = "";
+ let isActionsDropdown = (selector == "accountActions");
+
+ let isSyncing = false;
+ let isConnected = false;
+ let isEnabled = false;
+ let isInstalled = false;
+
+ if (accountsList.selectedItem !== null && !isNaN(accountsList.selectedItem.value)) {
+ //some item is selected
+ let selectedItem = accountsList.selectedItem;
+ selectedAccount = selectedItem.value;
+ selectedAccountName = selectedItem.childNodes[1].getAttribute("value");
+ isSyncing = TbSync.core.isSyncing(selectedAccount);
+ isConnected = TbSync.core.isConnected(selectedAccount);
+ isEnabled = TbSync.core.isEnabled(selectedAccount);
+ isInstalled = tbSyncAccounts.hasInstalledProvider(selectedAccount);
+ }
+
+ //hide if no accounts are avail (which is identical to no account selected)
+ if (isActionsDropdown) document.getElementById(selector + "SyncAllAccounts").hidden = (selectedAccount === null);
+
+ //hide if no account is selected
+ if (isActionsDropdown) document.getElementById(selector + "Separator").hidden = (selectedAccount === null);
+ document.getElementById(selector + "DeleteAccount").hidden = (selectedAccount === null);
+ document.getElementById(selector + "DisableAccount").hidden = (selectedAccount === null) || !isEnabled || !isInstalled;
+ document.getElementById(selector + "EnableAccount").hidden = (selectedAccount === null) || isEnabled || !isInstalled;
+ document.getElementById(selector + "SyncAccount").hidden = (selectedAccount === null) || !isConnected || !isInstalled;
+ document.getElementById(selector + "RetryConnectAccount").hidden = (selectedAccount === null) || isConnected || !isEnabled || !isInstalled;
+
+ if (document.getElementById(selector + "ShowEventLog")) {
+ document.getElementById(selector + "ShowEventLog").hidden = false;
+ document.getElementById(selector + "ShowEventLog").disabled = false;
+ }
+
+ if (selectedAccount !== null) {
+ //disable if currently syncing (and displayed)
+ document.getElementById(selector + "DeleteAccount").disabled = isSyncing;
+ document.getElementById(selector + "DisableAccount").disabled = isSyncing;
+ document.getElementById(selector + "EnableAccount").disabled = isSyncing;
+ document.getElementById(selector + "SyncAccount").disabled = isSyncing;
+ //adjust labels - only in global actions dropdown
+ if (isActionsDropdown) document.getElementById(selector + "DeleteAccount").label = TbSync.getString("accountacctions.delete").replace("##accountname##", selectedAccountName);
+ if (isActionsDropdown) document.getElementById(selector + "SyncAccount").label = TbSync.getString("accountacctions.sync").replace("##accountname##", selectedAccountName);
+ if (isActionsDropdown) document.getElementById(selector + "EnableAccount").label = TbSync.getString("accountacctions.enable").replace("##accountname##", selectedAccountName);
+ if (isActionsDropdown) document.getElementById(selector + "DisableAccount").label = TbSync.getString("accountacctions.disable").replace("##accountname##", selectedAccountName);
+ }
+ },
+
+ synchronizeAccount: function () {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+ if (accountsList.selectedItem !== null && !isNaN(accountsList.selectedItem.value) && !TbSync.core.isSyncing(accountsList.selectedItem.value)) {
+ if (tbSyncAccounts.hasInstalledProvider(accountsList.selectedItem.value)) {
+ TbSync.core.syncAccount(accountsList.selectedItem.value);
+ }
+ }
+ },
+
+ deleteAccount: function () {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+ if (accountsList.selectedItem !== null && !isNaN(accountsList.selectedItem.value) && !TbSync.core.isSyncing(accountsList.selectedItem.value)) {
+ let nextAccount = -1;
+ if (accountsList.selectedIndex > 0) {
+ //first try to select the item after this one, otherwise take the one before
+ if (accountsList.selectedIndex + 1 < accountsList.getRowCount()) nextAccount = accountsList.getItemAtIndex(accountsList.selectedIndex + 1).value;
+ else nextAccount = accountsList.getItemAtIndex(accountsList.selectedIndex - 1).value;
+ }
+
+ if (!tbSyncAccounts.hasInstalledProvider(accountsList.selectedItem.value)) {
+ if (confirm(TbSync.getString("prompt.Erase").replace("##accountName##", accountsList.selectedItem.getAttribute("label")))) {
+ //delete account and all folders from db
+ TbSync.db.removeAccount(accountsList.selectedItem.value);
+ //update list
+ this.updateAccountsList(nextAccount);
+ }
+ } else if (confirm(TbSync.getString("prompt.DeleteAccount").replace("##accountName##", accountsList.selectedItem.getAttribute("label")))) {
+ //cache all folders and remove associated targets
+ TbSync.core.disableAccount(accountsList.selectedItem.value);
+
+ // the following call might fail, as not all providers provide that method, it was mainly added to cleanup stored passwords
+ try {
+ let accountData = new TbSync.AccountData(accountsList.selectedItem.value);
+ TbSync.providers[accountData.getAccountProperty("provider")].Base.onDeleteAccount(accountData);
+ } catch (e) { Components.utils.reportError(e);}
+
+ //delete account and all folders from db
+ TbSync.db.removeAccount(accountsList.selectedItem.value);
+ //update list
+ this.updateAccountsList(nextAccount);
+ }
+ }
+ },
+
+
+
+ /* * *
+ * Observer to catch update list request (upon provider load/unload)
+ */
+ updateAccountsListObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //aData is the accountID to be selected
+ //if missing, it will try to not change selection
+ tbSyncAccounts.updateAccountsList(aData);
+ }
+ },
+
+ updateProviderListObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //aData is a provider
+ tbSyncAccounts.updateAvailableProvider(aData);
+ }
+ },
+
+ toggleEnableState: function () {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+
+ if (accountsList.selectedItem !== null && !isNaN(accountsList.selectedItem.value) && !TbSync.core.isSyncing(accountsList.selectedItem.value)) {
+ let isConnected = TbSync.core.isConnected(accountsList.selectedItem.value);
+ if (!isConnected || window.confirm(TbSync.getString("prompt.Disable"))) {
+ tbSyncAccounts.toggleAccountEnableState(accountsList.selectedItem.value);
+ }
+ }
+ },
+
+ /* * *
+ * Observer to catch enable state toggle
+ */
+ toggleEnableStateObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ tbSyncAccounts.toggleAccountEnableState(aData);
+ }
+ },
+
+ //is not prompting, this is doing the actual toggle
+ toggleAccountEnableState: function (accountID) {
+ if (tbSyncAccounts.hasInstalledProvider(accountID)) {
+ let isEnabled = TbSync.core.isEnabled(accountID);
+
+ if (isEnabled) {
+ //we are enabled and want to disable (do not ask, if not connected)
+ TbSync.core.disableAccount(accountID);
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountSettingsGui", accountID);
+ tbSyncAccounts.updateAccountStatus(accountID);
+ } else {
+ //we are disabled and want to enabled
+ TbSync.core.enableAccount(accountID);
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountSettingsGui", accountID);
+ TbSync.core.syncAccount(accountID);
+ }
+ }
+ },
+
+ /* * *
+ * Observer to catch synstate changes and to update account icons
+ */
+ updateAccountSyncStateObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ if (aData) {
+ //since we want rotating arrows on each syncstate change, we need to run this on each syncstate
+ tbSyncAccounts.updateAccountStatus(aData);
+ }
+ }
+ },
+
+ setStatusImage: function (accountID, obj) {
+ let statusImage = this.getStatusImage(accountID, obj.src);
+ if (statusImage != obj.src) {
+ obj.src = statusImage;
+ }
+ },
+
+ getStatusImage: function (accountID, current = "") {
+ let src = "";
+
+ if (!tbSyncAccounts.hasInstalledProvider(accountID)) {
+ src = "error16.png";
+ } else {
+ switch (TbSync.db.getAccountProperty(accountID, "status").split(".")[0]) {
+ case "success":
+ src = "tick16.png";
+ break;
+
+ case "disabled":
+ src = "disabled16.png";
+ break;
+
+ case "info":
+ case "notsyncronized":
+ case "modified":
+ src = "info16.png";
+ break;
+
+ case "warning":
+ src = "warning16.png";
+ break;
+
+ case "syncing":
+ switch (current.replace("chrome://tbsync/content/skin/","")) {
+ case "sync16_1.png":
+ src = "sync16_2.png";
+ break;
+ case "sync16_2.png":
+ src = "sync16_3.png";
+ break;
+ case "sync16_3.png":
+ src = "sync16_4.png";
+ break;
+ case "sync16_4.png":
+ src = "sync16_1.png";
+ break;
+ default:
+ src = "sync16_1.png";
+ TbSync.core.getSyncDataObject(accountID).accountManagerLastUpdated = 0;
+ break;
+ }
+ if ((Date.now() - TbSync.core.getSyncDataObject(accountID).accountManagerLastUpdated) < 300) {
+ return current;
+ }
+ TbSync.core.getSyncDataObject(accountID).accountManagerLastUpdated = Date.now();
+ break;
+
+ default:
+ src = "error16.png";
+ }
+ }
+
+ return "chrome://tbsync/content/skin/" + src;
+ },
+
+ updateAccountLogo: function (id) {
+ let accountData = new TbSync.AccountData(id);
+ let listItem = document.getElementById("tbSyncAccounts.accounts." + id);
+ if (listItem) {
+ let obj = listItem.childNodes[0];
+ obj.src = tbSyncAccounts.hasInstalledProvider(id) ? TbSync.providers[accountData.getAccountProperty("provider")].Base.getProviderIcon(16, accountData) : "chrome://tbsync/content/skin/provider16.png";
+ }
+ },
+
+ updateAccountStatus: function (id) {
+ let listItem = document.getElementById("tbSyncAccounts.accounts." + id);
+ if (listItem) {
+ let obj = listItem.childNodes[2];
+ this.setStatusImage(id, obj);
+ }
+ },
+
+ updateAccountNameObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ let pos = aData.indexOf(":");
+ let id = aData.substring(0, pos);
+ let name = aData.substring(pos+1);
+ tbSyncAccounts.updateAccountName (id, name);
+ }
+ },
+
+ updateAccountName: function (id, name) {
+ let listItem = document.getElementById("tbSyncAccounts.accounts." + id);
+ if (listItem.childNodes[1].getAttribute("value") != name) {
+ listItem.childNodes[1].setAttribute("value", name);
+ }
+ },
+
+ updateAvailableProvider: function (provider = null) {
+ //either add/remove a specific provider, or rebuild the list from scratch
+ if (provider) {
+ //update single provider entry
+ tbSyncAccounts.updateAddMenuEntry(provider);
+ } else {
+ //add default providers
+ for (let provider in TbSync.providers.defaultProviders) {
+ tbSyncAccounts.updateAddMenuEntry(provider);
+ }
+ //update/add all remaining installed providers
+ for (let provider in TbSync.providers.loadedProviders) {
+ tbSyncAccounts.updateAddMenuEntry(provider);
+ }
+ }
+
+ this.updateAccountsList();
+
+ let selectedAccount = this.getSelectedAccount();
+ if (selectedAccount !== null && TbSync.db.getAccountProperty(selectedAccount, "provider") == provider) {
+ tbSyncAccounts.loadSelectedAccount();
+ }
+ },
+
+ updateAccountsList: function (accountToSelect = null) {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+ let accounts = TbSync.db.getAccounts();
+
+ // try to keep the currently selected account, if accountToSelect is not given
+ if (accountToSelect === null) {
+ let s = accountsList.getItemAtIndex(accountsList.selectedIndex);
+ if (s) {
+ // there is an entry selected, do not change it
+ accountToSelect = s.value;
+ }
+ }
+
+ if (accounts.allIDs.length > null) {
+
+ //get current accounts in list and remove entries of accounts no longer there
+ let listedAccounts = [];
+ for (let i=accountsList.getRowCount()-1; i>=0; i--) {
+ let item = accountsList.getItemAtIndex(i);
+ listedAccounts.push(item.value);
+ if (accounts.allIDs.indexOf(item.value) == -1) {
+ item.remove();
+ }
+ }
+
+ //accounts array is without order, extract keys (ids) and loop over keys
+ for (let i = 0; i < accounts.allIDs.length; i++) {
+
+ if (listedAccounts.indexOf(accounts.allIDs[i]) == -1) {
+ //add all missing accounts (always to the end of the list)
+ let newListItem = document.createXULElement("richlistitem");
+ newListItem.setAttribute("id", "tbSyncAccounts.accounts." + accounts.allIDs[i]);
+ newListItem.setAttribute("value", accounts.allIDs[i]);
+ newListItem.setAttribute("align", "center");
+ newListItem.setAttribute("label", accounts.data[accounts.allIDs[i]].accountname);
+ newListItem.setAttribute("style", "padding: 5px 0px;");
+ newListItem.setAttribute("ondblclick", "tbSyncAccounts.toggleEnableState();");
+
+ //add icon (use "install provider" icon, if provider not installed)
+ let itemType = document.createXULElement("image");
+ //itemType.setAttribute("width", "16");
+ //itemType.setAttribute("height", "16");
+ itemType.setAttribute("style", "margin: 0px 0px 0px 5px; width:16px; height:16px");
+ newListItem.appendChild(itemType);
+
+ //add account name
+ let itemLabel = document.createXULElement("label");
+ itemLabel.setAttribute("flex", "1");
+ newListItem.appendChild(itemLabel);
+
+ //add account status
+ let itemStatus = document.createXULElement("image");
+ //itemStatus.setAttribute("width", "16");
+ //itemStatus.setAttribute("height", "16");
+ itemStatus.setAttribute("style", "margin: 0px 5px; width:16px; height:16px");
+ newListItem.appendChild(itemStatus);
+
+ accountsList.appendChild(newListItem);
+ }
+
+ //update/set actual values
+ this.updateAccountName(accounts.allIDs[i], accounts.data[accounts.allIDs[i]].accountname);
+ this.updateAccountStatus(accounts.allIDs[i]);
+ this.updateAccountLogo(accounts.allIDs[i]);
+ }
+
+ //find selected item
+ for (let i=0; i<accountsList.getRowCount(); i++) {
+ if (accountToSelect === null || accountToSelect == accountsList.getItemAtIndex(i).value) {
+ accountsList.selectedIndex = i;
+ accountsList.ensureIndexIsVisible(i);
+ break;
+ }
+ }
+
+ } else {
+ //No defined accounts, empty accounts list and load dummy
+ for (let i=accountsList.getRowCount()-1; i>=0; i--) {
+ accountsList.getItemAtIndex(i).remove();
+ }
+ document.getElementById("tbSyncAccounts.contentFrame").setAttribute("src", "chrome://tbsync/content/manager/noaccounts.xhtml");
+ }
+ },
+
+ updateAddMenuEntry: function (provider) {
+ let isDefault = TbSync.providers.defaultProviders.hasOwnProperty(provider);
+ let isInstalled = TbSync.providers.loadedProviders.hasOwnProperty(provider);
+
+ let entry = document.getElementById("addMenuEntry_" + provider);
+ if (entry === null) {
+ //add basic menu entry
+ let newItem = window.document.createXULElement("menuitem");
+ newItem.setAttribute("id", "addMenuEntry_" + provider);
+ newItem.setAttribute("value", provider);
+ newItem.setAttribute("class", "menuitem-iconic");
+ newItem.addEventListener("click", function () {tbSyncAccounts.addAccountAction(provider)}, false);
+ newItem.setAttribute("hidden", true);
+ entry = window.document.getElementById("accountActionsAddAccount").appendChild(newItem);
+ }
+
+ //Update label, icon and hidden according to isDefault and isInstalled
+ if (isInstalled) {
+ entry.setAttribute("label", TbSync.providers[provider].Base.getProviderName());
+ entry.setAttribute("image", TbSync.providers[provider].Base.getProviderIcon(16));
+ entry.setAttribute("hidden", false);
+ } else if (isDefault) {
+ entry.setAttribute("label", TbSync.providers.defaultProviders[provider].name);
+ entry.setAttribute("image", "chrome://tbsync/content/skin/provider16.png");
+ entry.setAttribute("hidden", false);
+ } else {
+ entry.setAttribute("hidden", true);
+ }
+ },
+
+ getSelectedAccount: function () {
+ let accountsList = document.getElementById("tbSyncAccounts.accounts");
+ if (accountsList.selectedItem !== null && !isNaN(accountsList.selectedItem.value)) {
+ //get id of selected account from value of selectedItem
+ return accountsList.selectedItem.value;
+ }
+ return null;
+ },
+
+ //load the pref page for the currently selected account (triggered by onSelect)
+ loadSelectedAccount: function () {
+ let selectedAccount = this.getSelectedAccount();
+
+ if (selectedAccount !== null) { //account id could be 0, so need to check for null explicitly
+ let provider = TbSync.db.getAccountProperty(selectedAccount, "provider");
+ if (tbSyncAccounts.hasInstalledProvider(selectedAccount)) {
+ document.getElementById("tbSyncAccounts.contentFrame").setAttribute("src", "chrome://tbsync/content/manager/editAccount.xhtml?provider="+provider+"&id=" + selectedAccount);
+ } else {
+ document.getElementById("tbSyncAccounts.contentFrame").setAttribute("src", "chrome://tbsync/content/manager/missingProvider.xhtml?provider="+provider);
+ }
+ }
+ },
+
+
+
+
+ addAccountAction: function (provider) {
+ let isDefault = TbSync.providers.defaultProviders.hasOwnProperty(provider);
+ let isInstalled = TbSync.providers.loadedProviders.hasOwnProperty(provider);
+
+ if (isInstalled) {
+ tbSyncAccounts.addAccount(provider);
+ } else if (isDefault) {
+ tbSyncAccounts.installProvider(provider);
+ }
+ },
+
+ addAccount: function (provider) {
+ TbSync.providers.loadedProviders[provider].createAccountWindow = window.openDialog(TbSync.providers[provider].Base.getCreateAccountWindowUrl(), "TbSyncNewAccountWindow", "centerscreen,resizable=no");
+ TbSync.providers.loadedProviders[provider].createAccountWindow.addEventListener("unload", function () { TbSync.manager.prefWindowObj.focus(); });
+ },
+
+ installProvider: function (provider) {
+ for (let i=0; i<TbSync.AccountManagerTabs.length; i++) {
+ TbSync.manager.prefWindowObj.document.getElementById("tbSyncAccountManager.t" + i).setAttribute("active","false");
+ }
+ TbSync.manager.prefWindowObj.document.getElementById("tbSyncAccountManager.installProvider").hidden=false;
+ TbSync.manager.prefWindowObj.document.getElementById("tbSyncAccountManager.installProvider").setAttribute("active","true");
+ TbSync.manager.prefWindowObj.document.getElementById("tbSyncAccountManager.contentWindow").setAttribute("src", "chrome://tbsync/content/manager/installProvider.xhtml?provider="+provider);
+ },
+
+};
diff --git a/content/manager/accounts.xhtml b/content/manager/accounts.xhtml
new file mode 100644
index 0000000..663c835
--- /dev/null
+++ b/content/manager/accounts.xhtml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="tbSyncAccounts.onload();"
+ onunload="tbSyncAccounts.onunload();"
+ title="TbSync Account Settings" >
+
+ <popupset>
+ <menupopup id="tbsync.accountmanger.ContextMenu" onpopupshowing="tbSyncAccounts.updateDropdown('contextMenu');">
+ <menuitem id="contextMenuRetryConnectAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/connect16.png"
+ label="__TBSYNCMSG_manager.RetryConnectAccount__"
+ oncommand="tbSyncAccounts.synchronizeAccount();"/>
+ <menuitem id="contextMenuSyncAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/sync16.png"
+ label="__TBSYNCMSG_manager.SynchronizeAccount__"
+ oncommand="tbSyncAccounts.synchronizeAccount();"/>
+ <menuitem id="contextMenuEnableAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/connect16.png"
+ label="__TBSYNCMSG_manager.EnableAccount__"
+ oncommand="tbSyncAccounts.toggleEnableState();"/>
+ <menuitem id="contextMenuDisableAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/disabled16.png"
+ label="__TBSYNCMSG_manager.DisableAccount__"
+ oncommand="tbSyncAccounts.toggleEnableState();"/>
+ <menuitem id="contextMenuDeleteAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/del16.png"
+ label="__TBSYNCMSG_manager.DeleteAccount__"
+ oncommand="tbSyncAccounts.deleteAccount();"/>
+ </menupopup>
+ </popupset>
+
+ <hbox flex="1">
+ <vbox width="200">
+ <richlistbox
+ id="tbSyncAccounts.accounts"
+ flex="1"
+ style="margin: 0 1px; width: 200px;"
+ seltype="single"
+ context="tbsync.accountmanger.ContextMenu"
+ onkeypress="if (event.keyCode == 46) {tbSyncAccounts.deleteAccount();}"
+ onselect="tbSyncAccounts.loadSelectedAccount();">
+ <listheader style="border-bottom: 1px solid lightgrey;">
+ <treecol style="font-weight:bold;" label="" width="26" flex="0" />
+ <treecol style="font-weight:bold;" label="__TBSYNCMSG_manager.accounts__" flex="1" />
+ <treecol style="font-weight:bold;text-align:right;" label="__TBSYNCMSG_manager.status__" flex="0" />
+ </listheader>
+ </richlistbox>
+ <hbox style="margin:1ex 0 0 0">
+ <vbox style="margin:0" flex="1">
+ <button
+ id="tbSyncAccounts.btnAccountActions"
+ label="__TBSYNCMSG_manager.AccountActions__"
+ style="margin:0"
+ type="menu">
+ <menupopup id="accountActionsDropdown" onpopupshowing="tbSyncAccounts.updateDropdown('accountActions');">
+ <menu
+ class="menu-iconic"
+ image="chrome://tbsync/content/skin/add16.png"
+ label="__TBSYNCMSG_manager.AddAccount__">
+ <menupopup id="accountActionsAddAccount" />
+ </menu>
+ <menuitem id="accountActionsSyncAllAccounts"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/sync16.png"
+ label="__TBSYNCMSG_manager.SyncAll__"
+ oncommand="TbSync.core.syncAllAccounts();"/>
+ <menuitem id="accountActionsShowEventLog"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/warning16.png"
+ label="__TBSYNCMSG_manager.ShowEventLog__"
+ oncommand="TbSync.eventlog.open()"/>
+ <menuseparator id="accountActionsSeparator"/>
+ <menuitem id="accountActionsDeleteAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/del16.png"
+ label="__TBSYNCMSG_manager.DeleteAccount__"
+ oncommand="tbSyncAccounts.deleteAccount();"/>
+ <menuitem id="accountActionsDisableAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/disabled16.png"
+ label="__TBSYNCMSG_manager.DisableAccount__"
+ oncommand="tbSyncAccounts.toggleEnableState();"/>
+ <menuitem id="accountActionsEnableAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/connect16.png"
+ label="__TBSYNCMSG_manager.EnableAccount__"
+ oncommand="tbSyncAccounts.toggleEnableState();"/>
+ <menuitem id="accountActionsSyncAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/sync16.png"
+ label="__TBSYNCMSG_manager.SynchronizeAccount__"
+ oncommand="tbSyncAccounts.synchronizeAccount();"/>
+ <menuitem id="accountActionsRetryConnectAccount"
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/connect16.png"
+ label="__TBSYNCMSG_manager.RetryConnectAccount__"
+ oncommand="tbSyncAccounts.synchronizeAccount();"/>
+ </menupopup>
+ </button>
+ </vbox>
+ </hbox>
+ </vbox>
+ <browser id="tbSyncAccounts.contentFrame" type="chrome" src="" disablehistory="true" flex="1" style="margin-left:12px;"/>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accounts.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/addonoptions.xhtml b/content/manager/addonoptions.xhtml
new file mode 100644
index 0000000..8ee9c10
--- /dev/null
+++ b/content/manager/addonoptions.xhtml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="__TBSYNCMSG_manager.title__"
+ onload="tbSyncAccountManager.onloadoptions();"
+ onunload="tbSyncAccountManager.onunloadoptions();"
+ width="180" height="80" >
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/catman.xhtml b/content/manager/catman.xhtml
new file mode 100644
index 0000000..ce78783
--- /dev/null
+++ b/content/manager/catman.xhtml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Category Manager" >
+
+ <hbox flex="1" id="mainframe">
+ <vbox flex="1">
+ <html:p>
+ __TBSYNCMSG_manager.catman.text__
+ </html:p>
+
+ <html:p onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="TbSync.manager.openLink('https://addons.thunderbird.net/addon/categorymanager/');" style="color:blue;text-decoration: underline;padding-left:1em;">
+ https://addons.thunderbird.net/addon/categorymanager/
+ </html:p>
+ </vbox>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/editAccount.js b/content/manager/editAccount.js
new file mode 100644
index 0000000..ff4db50
--- /dev/null
+++ b/content/manager/editAccount.js
@@ -0,0 +1,391 @@
+/*
+ * 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncAccountSettings = {
+
+ accountID: null,
+ provider: null,
+ settings: null,
+ updateTimer: Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer),
+
+ updateFolderListObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //only run if is request for this account and main frame is visible
+ let accountID = aData;
+ if (accountID == tbSyncAccountSettings.accountID && !document.getElementById('tbsync.accountsettings.frame').hidden) {
+ //make sure, folderlist is visible, otherwise our updates will be discarded (may cause errors)
+ tbSyncAccountSettings.updateFolderList();
+ tbSyncAccountSettings.updateGui();
+ }
+ }
+ },
+
+ reloadAccountSettingObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //only run if is request for this account and main frame is visible
+ let data = JSON.parse(aData);
+ if (data.accountID == tbSyncAccountSettings.accountID && !document.getElementById('tbsync.accountsettings.frame').hidden) {
+ tbSyncAccountSettings.reloadSetting(data.setting);
+ }
+ }
+ },
+
+ updateGuiObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //only run if is request for this account and main frame is visible
+ let accountID = aData;
+ if (accountID == tbSyncAccountSettings.accountID && !document.getElementById('tbsync.accountsettings.frame').hidden) {
+ tbSyncAccountSettings.updateGui();
+ }
+ }
+ },
+
+ updateSyncstateObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //only run if is request for this account and main frame is visible
+ let accountID = aData;
+ if (accountID == tbSyncAccountSettings.accountID && !document.getElementById('tbsync.accountsettings.frame').hidden) {
+ let syncstate = TbSync.core.getSyncDataObject(accountID).getSyncState().state;
+ if (syncstate == "accountdone") {
+ tbSyncAccountSettings.updateGui();
+ } else {
+ tbSyncAccountSettings.updateSyncstate();
+ }
+ }
+ }
+ },
+
+ onload: function () {
+ //load observers
+ Services.obs.addObserver(tbSyncAccountSettings.updateFolderListObserver, "tbsync.observer.manager.updateFolderList", false);
+ Services.obs.addObserver(tbSyncAccountSettings.updateGuiObserver, "tbsync.observer.manager.updateAccountSettingsGui", false);
+ Services.obs.addObserver(tbSyncAccountSettings.reloadAccountSettingObserver, "tbsync.observer.manager.reloadAccountSetting", false);
+ Services.obs.addObserver(tbSyncAccountSettings.updateSyncstateObserver, "tbsync.observer.manager.updateSyncstate", false);
+ //get the selected account from the loaded URI
+ tbSyncAccountSettings.accountID = window.location.toString().split("id=")[1];
+ tbSyncAccountSettings.accountData = new TbSync.AccountData(tbSyncAccountSettings.accountID);
+
+ //get information for that acount
+ tbSyncAccountSettings.provider = TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, "provider");
+ tbSyncAccountSettings.settings = Object.keys(TbSync.providers.getDefaultAccountEntries(tbSyncAccountSettings.provider)).sort();
+
+ //add header to folderlist
+ let header = TbSync.providers[tbSyncAccountSettings.provider].folderList.getHeader();
+ let folderlistHeader = window.document.getElementById('tbsync.accountsettings.folderlist.header');
+ for (let h=0; h < header.length; h++) {
+ let listheader = window.document.createXULElement("treecol");
+ for (let a in header[h]) {
+ if (header[h].hasOwnProperty(a)) {
+ listheader.setAttribute(a, header[h][a]);
+ }
+ }
+ folderlistHeader.appendChild(listheader);
+ }
+
+ //load overlays from the provider (if any)
+ TbSync.messenger.overlayManager.injectAllOverlays(window, "chrome://tbsync/content/manager/editAccount.xhtml?provider=" + tbSyncAccountSettings.provider);
+ if (window.tbSyncEditAccountOverlay && window.tbSyncEditAccountOverlay.hasOwnProperty("onload")) {
+ tbSyncEditAccountOverlay.onload(window, new TbSync.AccountData(tbSyncAccountSettings.accountID));
+ }
+ tbSyncAccountSettings.loadSettings();
+
+ //done, folderlist must be updated while visible
+ document.getElementById('tbsync.accountsettings.frame').hidden = false;
+ tbSyncAccountSettings.updateFolderList();
+
+ if (Services.appinfo.OS == "Darwin") { //we might need to find a way to detect MacOS like styling, other themes move the header bar into the tabpanel as well
+ document.getElementById('manager.tabpanels').style["padding-top"] = "3ex";
+ }
+ },
+
+
+ onunload: function () {
+ tbSyncAccountSettings.updateTimer.cancel();
+ if (!document.getElementById('tbsync.accountsettings.frame').hidden) {
+ Services.obs.removeObserver(tbSyncAccountSettings.updateFolderListObserver, "tbsync.observer.manager.updateFolderList");
+ Services.obs.removeObserver(tbSyncAccountSettings.updateGuiObserver, "tbsync.observer.manager.updateAccountSettingsGui");
+ Services.obs.removeObserver(tbSyncAccountSettings.reloadAccountSettingObserver, "tbsync.observer.manager.reloadAccountSetting");
+ Services.obs.removeObserver(tbSyncAccountSettings.updateSyncstateObserver, "tbsync.observer.manager.updateSyncstate");
+ }
+ },
+
+
+ folderListVisible: function () {
+ let box = document.getElementById('tbsync.accountsettings.folderlist').getBoundingClientRect();
+ let visible = box.width && box.height;
+ return visible;
+ },
+
+
+ reloadSetting: function (setting) {
+ let pref = document.getElementById("tbsync.accountsettings.pref." + setting);
+ let label = document.getElementById("tbsync.accountsettings.label." + setting);
+
+ if (pref) {
+ //is this a checkbox?
+ if ((pref.tagName == "checkbox") || ((pref.tagName == "input") && (pref.type == "checkbox"))) {
+ //BOOL
+ if (TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, setting)) pref.setAttribute("checked", true);
+ else pref.removeAttribute("checked");
+ } else {
+ //Not BOOL
+ pref.value = TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, setting);
+ }
+ }
+ },
+
+
+ /**
+ * Run through all defined TbSync settings and if there is a corresponding
+ * field in the settings dialog, fill it with the stored value.
+ */
+ loadSettings: function () {
+ for (let i=0; i < tbSyncAccountSettings.settings.length; i++) {
+ let pref = document.getElementById("tbsync.accountsettings.pref." + tbSyncAccountSettings.settings[i]);
+ let label = document.getElementById("tbsync.accountsettings.label." + tbSyncAccountSettings.settings[i]);
+
+ if (pref) {
+ //is this a checkbox?
+ let event = "blur";
+ if ((pref.tagName == "checkbox") || ((pref.tagName == "input") && (pref.type == "checkbox"))) {
+ //BOOL
+ if (TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, tbSyncAccountSettings.settings[i])) pref.setAttribute("checked", true);
+ else pref.removeAttribute("checked");
+ event = "command";
+ } else {
+ //Not BOOL
+ if (pref.tagName == "menulist") {
+ pref.value = TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, tbSyncAccountSettings.settings[i]);
+ event = "command";
+ } else {
+ pref.setAttribute("value", TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, tbSyncAccountSettings.settings[i]));
+ }
+ }
+
+ pref.addEventListener(event, function() {tbSyncAccountSettings.instantSaveSetting(this)});
+ }
+ }
+
+ tbSyncAccountSettings.updateGui();
+ },
+
+ updateGui: function () {
+ let status = TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, "status");
+
+ let isConnected = TbSync.core.isConnected(tbSyncAccountSettings.accountID);
+ let isEnabled = TbSync.core.isEnabled(tbSyncAccountSettings.accountID);
+ let isSyncing = TbSync.core.isSyncing(tbSyncAccountSettings.accountID);
+
+ { //disable settings if connected or syncing
+ let items = document.getElementsByClassName("lockIfConnected");
+ for (let i=0; i < items.length; i++) {
+ if (isConnected || isSyncing || items[i].getAttribute("alwaysDisabled") == "true") {
+ items[i].setAttribute("disabled", true);
+ items[i].style["color"] = "darkgrey";
+ } else {
+ items[i].removeAttribute("disabled");
+ items[i].style["color"] = "black";
+ }
+ }
+ }
+
+ document.getElementById('tbsync.accountsettings.connectbtn.container').hidden = !(isEnabled && !isConnected && !isSyncing);
+ //currently we use a fixed button which is hidden during sync
+ //document.getElementById('tbsync.accountsettings.connectbtn').label = TbSync.getString("manager." + (isSyncing ? "connecting" : "tryagain"));
+
+ { //show elements if connected (this also hides/unhides the folderlist)
+ let items = document.getElementsByClassName("showIfConnected");
+ for (let i=0; i < items.length; i++) {
+ items[i].hidden = !isConnected;
+ }
+ }
+
+ { //show elements if enabled
+ let items = document.getElementsByClassName("showIfEnabled");
+ for (let i=0; i < items.length; i++) {
+ items[i].hidden = !isEnabled;
+ }
+ }
+
+ document.getElementById('tbsync.accountsettings.enabled').checked = isEnabled;
+ document.getElementById('tbsync.accountsettings.enabled').disabled = isSyncing;
+ document.getElementById('tbsync.accountsettings.folderlist').disabled = isSyncing;
+ document.getElementById('tbsync.accountsettings.syncbtn').disabled = isSyncing;
+ document.getElementById('tbsync.accountsettings.connectbtn').disabled = isSyncing;
+
+ tbSyncAccountSettings.updateSyncstate();
+
+ //change color of syncstate according to status
+ let showEventLogButton = false;
+ switch (status) {
+ case "success":
+ case "disabled":
+ case "syncing":
+ document.getElementById("syncstate").removeAttribute("style");
+ break;
+
+ case "notsyncronized":
+ document.getElementById("syncstate").setAttribute("style","color: red");
+ break;
+
+ default:
+ document.getElementById("syncstate").setAttribute("style","color: red");
+ showEventLogButton = TbSync.eventlog.get(tbSyncAccountSettings.accountID).length > 0;
+ }
+ document.getElementById('tbsync.accountsettings.eventlogbtn').hidden = !showEventLogButton;
+ },
+
+ updateSyncstate: function () {
+ tbSyncAccountSettings.updateTimer.cancel();
+
+ // if this account is beeing synced, display syncstate, otherwise print status
+ let status = TbSync.db.getAccountProperty(tbSyncAccountSettings.accountID, "status");
+ let isSyncing = TbSync.core.isSyncing(tbSyncAccountSettings.accountID);
+ let isConnected = TbSync.core.isConnected(tbSyncAccountSettings.accountID);
+ let isEnabled = TbSync.core.isEnabled(tbSyncAccountSettings.accountID);
+ let syncdata = TbSync.core.getSyncDataObject(tbSyncAccountSettings.accountID);
+
+ if (isSyncing) {
+ let accounts = TbSync.db.getAccounts().data;
+
+ let s = syncdata.getSyncState();
+ let syncstate = s.state;
+ let synctime = s.timestamp;
+
+ let msg = TbSync.getString("syncstate." + syncstate, tbSyncAccountSettings.provider);
+
+ if (syncstate.split(".")[0] == "send") {
+ // append timeout countdown
+ let diff = Date.now() - synctime;
+ if (diff > 2000) msg = msg + " (" + Math.round((TbSync.providers[tbSyncAccountSettings.provider].Base.getConnectionTimeout(tbSyncAccountSettings.accountData) - diff)/1000) + "s)";
+ // re-schedule update, if this is a waiting syncstate
+ tbSyncAccountSettings.updateTimer.init(tbSyncAccountSettings.updateSyncstate, 1000, 0);
+ }
+ document.getElementById("syncstate").textContent = msg;
+ } else {
+ let localized = TbSync.getString("status." + (isEnabled ? status : "disabled"), tbSyncAccountSettings.provider);
+ document.getElementById("syncstate").textContent = localized;
+ }
+
+
+ if (tbSyncAccountSettings.folderListVisible()) {
+ //update syncstates of folders in folderlist, if visible - remove obsolete entries while we are here
+ let folderData = TbSync.providers[tbSyncAccountSettings.provider].Base.getSortedFolders(tbSyncAccountSettings.accountData);
+ let folderList = document.getElementById("tbsync.accountsettings.folderlist");
+
+ for (let i=folderList.getRowCount()-1; i>=0; i--) {
+ let item = folderList.getItemAtIndex(i);
+ if (folderData.filter(f => f.folderID == item.folderData.folderID).length == 0) {
+ item.remove();
+ } else {
+ TbSync.providers[tbSyncAccountSettings.provider].folderList.updateRow(document, item, item.folderData);
+ }
+ }
+ }
+ },
+
+ updateFolderList: function () {
+ //get updated list of folderIDs
+ let folderData = TbSync.providers[tbSyncAccountSettings.provider].Base.getSortedFolders(tbSyncAccountSettings.accountData);
+
+ //remove entries from folderlist, which no longer exists and build reference array with current elements
+ let folderList = document.getElementById("tbsync.accountsettings.folderlist");
+ folderList.hidden=true;
+
+ let foldersElements = {};
+ for (let i=folderList.getRowCount()-1; i>=0; i--) {
+ if (folderData.filter(f => f.folderID == folderList.getItemAtIndex(i).folderData.folderID).length == 0) {
+ folderList.getItemAtIndex(i).remove();
+ } else {
+ foldersElements[folderList.getItemAtIndex(i).folderData.folderID] = folderList.getItemAtIndex(i);
+ }
+ }
+
+ //update folderlist
+ for (let i=0; i < folderData.length; i++) {
+ let nextItem = null;
+
+ //if this entry does not exist, create it
+ if (foldersElements.hasOwnProperty(folderData[i].folderID)) {
+ //get reference to current element
+ nextItem = foldersElements[folderData[i].folderID];
+ } else {
+ //add new entry, attach FolderData of this folder as folderData
+ nextItem = document.createXULElement("richlistitem");
+ nextItem.folderData = folderData[i];
+
+ //add row
+ nextItem.appendChild(TbSync.providers[tbSyncAccountSettings.provider].folderList.getRow(document, folderData[i]));
+ }
+
+ //add/move row and update its content
+ let addedItem = folderList.appendChild(nextItem);
+ TbSync.providers[tbSyncAccountSettings.provider].folderList.updateRow(document, addedItem, folderData[i]);
+
+ //ensureElementIsVisible also forces internal update of rowCount, which sometimes is not updated automatically upon appendChild
+ folderList.ensureElementIsVisible(addedItem);
+ }
+ folderList.hidden = false;
+ },
+
+
+
+
+
+ instantSaveSetting: function (field) {
+ let setting = field.id.replace("tbsync.accountsettings.pref.","");
+ let value = "";
+
+ if ((field.tagName == "checkbox") || ((field.tagName == "input") && (field.type == "checkbox"))) {
+ if (field.checked) value = true;
+ else value = false;
+ } else {
+ value = field.value;
+ }
+ TbSync.db.setAccountProperty(tbSyncAccountSettings.accountID, setting, value);
+
+ if (setting == "accountname") {
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountName", tbSyncAccountSettings.accountID + ":" + field.value);
+ }
+ TbSync.db.saveAccounts(); //write modified accounts to disk
+ },
+
+ toggleEnableState: function (element) {
+ if (!TbSync.core.isConnected(tbSyncAccountSettings.accountID)) {
+ //if not connected, we can toggle without prompt
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.toggleEnableState", tbSyncAccountSettings.accountID);
+ return;
+ }
+
+ if (window.confirm(TbSync.getString("prompt.Disable"))) {
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.toggleEnableState", tbSyncAccountSettings.accountID);
+ } else {
+ //invalid, toggle checkbox back
+ element.setAttribute("checked", true);
+ }
+ },
+
+
+ onFolderListContextMenuShowing: function () {
+ let folderList = document.getElementById("tbsync.accountsettings.folderlist");
+ let aFolderIsSelected = (!folderList.disabled && folderList.selectedItem !== null && folderList.selectedItem.value !== undefined);
+ let menupopup = document.getElementById("tbsync.accountsettings.FolderListContextMenu");
+
+ if (aFolderIsSelected) {
+ TbSync.providers[tbSyncAccountSettings.provider].folderList.onContextMenuShowing(window, folderList.selectedItem.folderData);
+ } else {
+ TbSync.providers[tbSyncAccountSettings.provider].folderList.onContextMenuShowing(window, null);
+ }
+ },
+
+};
diff --git a/content/manager/editAccount.xhtml b/content/manager/editAccount.xhtml
new file mode 100644
index 0000000..9381678
--- /dev/null
+++ b/content/manager/editAccount.xhtml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window id="tbsync.accountsettings"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ omscope="tbSyncAccountSettings"
+ onload="tbSyncAccountSettings.onload()"
+ onunload="tbSyncAccountSettings.onunload()"
+ title="" >
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/editAccount.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+
+ <popupset>
+ <menupopup
+ id="tbsync.accountsettings.FolderListContextMenu"
+ folderID=""
+ onpopupshowing="tbSyncAccountSettings.onFolderListContextMenuShowing();">
+ <menuitem
+ class="menuitem-iconic"
+ image="chrome://tbsync/content/skin/warning16.png"
+ label="__TBSYNCMSG_manager.ShowEventLog__"
+ oncommand="TbSync.eventlog.open(tbSyncAccountSettings.accountID, this.parentNode.getAttribute('folderID'));"/>
+ </menupopup>
+ </popupset>
+
+ <tabbox id="tbsync.accountsettings.frame" hidden="true" flex="1">
+
+ <tabs id="manager.tabs" orient="horizontal" value="">
+ <tab id="manager.tabs.status" label="__TBSYNCMSG_manager.tabs.status__" />
+ </tabs>
+
+ <tabpanels flex="1" id="manager.tabpanels" style="margin:0;padding:1ex;">
+ <tabpanel id="manager.tabpanels.status" orient="vertical"><!-- STATUS -->
+ <vbox flex="1">
+ <label class="header" style="margin-left:0; margin-bottom:1ex;" value="__TBSYNCMSG_manager.tabs.status.general__" />
+ <checkbox id="tbsync.accountsettings.enabled" oncommand="tbSyncAccountSettings.toggleEnableState(this);" label="__TBSYNCMSG_manager.tabs.status.enableThisAccount__" />
+
+ <vbox class="showIfEnabled" style="height:100px; overflow-x: hidden; overflow-y:hidden">
+ <hbox flex="1">
+ <vbox flex="1">
+ <label class="header" style="margin-left:0; margin-bottom:1ex; margin-top:2ex;" value="__TBSYNCMSG_manager.status__" />
+ <description id="syncstate"></description>
+ </vbox>
+ <vbox flex="0">
+ <label class="header" style="margin-left:0; margin-bottom:1ex; margin-top:1ex; visibility: hidden" value="nix" />
+ <button id="tbsync.accountsettings.eventlogbtn" label="__TBSYNCMSG_manager.ShowEventLog__" oncommand="TbSync.eventlog.open()" />
+ </vbox>
+ </hbox>
+ </vbox>
+
+ <vbox flex="1">
+ <vbox class="showIfConnected" flex="1">
+ <label style="margin-left:0; margin-bottom: 1ex; margin-top: 2ex" class="header" value="__TBSYNCMSG_manager.tabs.status.resources__"/>
+ <description>__TBSYNCMSG_manager.tabs.status.resources.intro__</description>
+ <richlistbox
+ id="tbsync.accountsettings.folderlist"
+ style="margin: 0 1px 1px 1ex;padding:0; height:225px; overflow-x: hidden;"
+ context="tbsync.accountsettings.FolderListContextMenu"
+ seltype="single">
+ <listheader id="tbsync.accountsettings.folderlist.header" style="border-bottom: 1px solid lightgrey;">
+ </listheader>
+ </richlistbox>
+ <vbox flex="0" style="margin:1ex 0 0 0;">
+ <hbox flex="1" align="center" pack="end">
+ <description style="text-align:right" flex="1" control="tbsync.accountsettings.pref.autosync" tooltiptext="__TBSYNCMSG_manager.tabs.status.never__">__TBSYNCMSG_manager.tabs.status.autotime__</description>
+ <html:input style="width:50px;margin-bottom:0; margin-top:0" id="tbsync.accountsettings.pref.autosync" tooltiptext="__TBSYNCMSG_manager.tabs.status.never__" />
+ <button id="tbsync.accountsettings.syncbtn" style="margin-right:0; margin-bottom:0; margin-top:0; padding: 0 1ex;" label="__TBSYNCMSG_manager.tabs.status.sync__" oncommand="TbSync.core.syncAccount(tbSyncAccountSettings.accountID)" />
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+
+ <hbox id="tbsync.accountsettings.connectbtn.container" flex="0" style="margin:1ex 0 0 0;" pack="end">
+ <button id="tbsync.accountsettings.connectbtn" style="margin-right:0; margin-bottom:0; margin-top:0; padding: 0 1ex;" label="__TBSYNCMSG_manager.tabs.status.tryagain__" oncommand="TbSync.core.syncAccount(tbSyncAccountSettings.accountID)" />
+ </hbox>
+
+ </vbox>
+ </tabpanel>
+ </tabpanels>
+
+ </tabbox>
+
+</window>
diff --git a/content/manager/eventlog/eventlog.js b/content/manager/eventlog/eventlog.js
new file mode 100644
index 0000000..eaa0b47
--- /dev/null
+++ b/content/manager/eventlog/eventlog.js
@@ -0,0 +1,158 @@
+/*
+ * 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncEventLog = {
+
+ onload: function () {
+ Services.obs.addObserver(tbSyncEventLog.updateEventLog, "tbsync.observer.eventlog.update", false);
+
+ let eventlog = document.getElementById('tbsync.eventlog');
+ eventlog.hidden = true;
+
+ //init list
+ let events = TbSync.eventlog.get();
+ for (let i=0; i < events.length; i++) {
+ let item = tbSyncEventLog.addLogEntry(events[i]);
+ eventlog.appendChild(item);
+ }
+ eventlog.hidden = false;
+ eventlog.ensureIndexIsVisible(eventlog.getRowCount()-1);
+ document.getElementById("tbsync.eventlog.clear").addEventListener("click", tbSyncEventLog.onclear);
+ document.getElementById("tbsync.eventlog.close").addEventListener("click", () => window.close());
+ },
+
+ onclear: function () {
+ TbSync.eventlog.clear();
+
+ let eventlog = document.getElementById('tbsync.eventlog');
+ eventlog.hidden = true;
+
+ for (let i=eventlog.getRowCount()-1; i>=0; i--) {
+ eventlog.getItemAtIndex(i).remove();
+ }
+
+ eventlog.hidden = false;
+ },
+
+ onunload: function () {
+ Services.obs.removeObserver(tbSyncEventLog.updateEventLog, "tbsync.observer.eventlog.update");
+ },
+
+ updateEventLog: {
+ observe: function (aSubject, aTopic, aData) {
+ let events = TbSync.eventlog.get();
+ if (events.length > 0) {
+ let eventlog = document.getElementById('tbsync.eventlog');
+ eventlog.hidden = true;
+
+ let item = tbSyncEventLog.addLogEntry(events[events.length-1]);
+ eventlog.appendChild(item);
+
+ eventlog.hidden = false;
+ eventlog.ensureIndexIsVisible(eventlog.getRowCount()-1);
+ }
+ }
+ },
+
+
+ addLogEntry: function (entry) {
+
+ //left column
+ let leftColumn = document.createXULElement("vbox");
+ //leftColumn.setAttribute("width", "24");
+ leftColumn.setAttribute("style", "width: 24px;");
+
+ let image = document.createXULElement("image");
+ let src = entry.type.endsWith("_rerun") ? "sync" : entry.type;
+ image.setAttribute("src", "chrome://tbsync/content/skin/" + src + "16.png");
+ image.setAttribute("style", "margin:4px 4px 4px 4px;");
+ leftColumn.appendChild(image);
+
+ //right column
+ let rightColumn = document.createXULElement("vbox");
+ rightColumn.setAttribute("flex","1");
+
+ let d = new Date(entry.timestamp);
+ let timestamp = document.createXULElement("description");
+ timestamp.setAttribute("flex", "1");
+ timestamp.setAttribute("class", "header");
+ timestamp.textContent = d.toLocaleTimeString();
+ rightColumn.appendChild(timestamp);
+
+ let hBox = document.createXULElement("hbox");
+ hBox.flex = "1";
+ let vBoxLeft = document.createXULElement("vbox");
+ vBoxLeft.flex = "1";
+ let vBoxRight = document.createXULElement("vbox");
+
+ let msg = document.createXULElement("description");
+ msg.setAttribute("flex", "1");
+ msg.setAttribute("class", "header");
+ msg.textContent = entry.message;
+ vBoxLeft.appendChild(msg);
+
+ if (entry.link) {
+ let link = document.createXULElement("button");
+ link.setAttribute("label", TbSync.getString("manager.help"));
+ link.setAttribute("oncommand", "TbSync.manager.openLink('" + entry.link + "')");
+ vBoxRight.appendChild(link);
+ }
+
+ hBox.appendChild(vBoxLeft);
+ hBox.appendChild(vBoxRight);
+ rightColumn.appendChild(hBox);
+
+ if (entry.accountname || entry.provider) {
+ let account = document.createXULElement("label");
+ if (entry.accountname) account.setAttribute("value", "Account: " + entry.accountname + (entry.provider ? " (" + entry.provider.toUpperCase() + ")" : ""));
+ else account.setAttribute("value", "Provider: " + entry.provider.toUpperCase());
+ rightColumn.appendChild(account);
+ }
+
+ if (entry.foldername) {
+ let folder = document.createXULElement("label");
+ folder.setAttribute("value", "Resource: " + entry.foldername);
+ rightColumn.appendChild(folder);
+ }
+
+ if (entry.details) {
+ let lines = entry.details.split("\n");
+ let line = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
+ line.setAttribute("readonly", "true");
+ line.setAttribute("wrap", "off");
+ line.setAttribute("rows", lines.length);
+ line.setAttribute("style", "font-family: monospace; font-size: 10px;");
+ line.setAttribute("class", "plain");
+ line.value = entry.details.trim();
+
+ let container = document.createXULElement("vbox");
+ container.setAttribute("style", "margin-left:1ex;margin-top:1ex;");
+ container.appendChild(line);
+
+ rightColumn.appendChild(container);
+ }
+
+ //columns
+ let columns = document.createXULElement("hbox");
+ columns.setAttribute("flex", "1");
+ columns.appendChild(leftColumn);
+ columns.appendChild(rightColumn);
+
+ //richlistitem
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("style", "padding:4px; border-bottom: 1px solid lightgrey;");
+ richlistitem.appendChild(columns);
+
+ return richlistitem;
+ },
+};
diff --git a/content/manager/eventlog/eventlog.xhtml b/content/manager/eventlog/eventlog.xhtml
new file mode 100644
index 0000000..b71af18
--- /dev/null
+++ b/content/manager/eventlog/eventlog.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ title="__TBSYNCMSG_eventlog.title__"
+ onload="tbSyncEventLog.onload();"
+ onunload="tbSyncEventLog.onunload();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" >
+
+ <script type="application/javascript" src="chrome://tbsync/content/manager/eventlog/eventlog.js"/>
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+
+ <vbox flex="1">
+ <richlistbox id="tbsync.eventlog" style="padding:5px; height:360px" seltype="single" disabled="true"/>
+ <hbox style="padding: 5px">
+ <vbox flex="1"></vbox>
+ <button id="tbsync.eventlog.clear" label="__TBSYNCMSG_eventlog.clear__" />
+ <button id="tbsync.eventlog.close" label="__TBSYNCMSG_eventlog.close__" />
+ </hbox>
+ </vbox>
+</window>
diff --git a/content/manager/help.xhtml b/content/manager/help.xhtml
new file mode 100644
index 0000000..675bf53
--- /dev/null
+++ b/content/manager/help.xhtml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="tbSyncAccountManager.getLogPref();"
+ title="Help" >
+
+ <hbox flex="1" id="mainframe">
+ <vbox flex="1">
+
+ <html:p>
+ <html:b>__TBSYNCMSG_manager.help.needhelp__</html:b><html:br/><html:br/>
+ __TBSYNCMSG_manager.help.wiki__
+ </html:p>
+
+ <html:p onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="TbSync.manager.openTranslatedLink('https://github.com/jobisoft/TbSync/wiki');" style="color:blue;text-decoration: underline;padding-left:1em;margin:0 0 24px 0">
+ https://github.com/jobisoft/TbSync/wiki
+ </html:p>
+
+ <html:p>
+ <html:b>__TBSYNCMSG_manager.help.foundabug__</html:b><html:br/><html:br/>
+ <html:span>__TBSYNCMSG_manager.help.fixit__</html:span>
+ </html:p>
+
+ <hbox align="center">
+ <label value="__TBSYNCMSG_manager.help.debugmode__" />
+ <menulist flex="0" id="tbSyncAccountManager.logLevel" oncommand="tbSyncAccountManager.toggleLogPref();">
+ <menupopup>
+ <menuitem label="__TBSYNCMSG_manager.help.debuglevel.0__" value="0" />
+ <menuitem label="__TBSYNCMSG_manager.help.debuglevel.1__" value="1" />
+ <menuitem label="__TBSYNCMSG_manager.help.debuglevel.2__" value="2" />
+ <menuitem label="__TBSYNCMSG_manager.help.debuglevel.3__" value="3" />
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <html:p>
+ __TBSYNCMSG_manager.help.createbugreportinfo__
+ </html:p>
+
+ <hbox>
+ <button style="margin:0 0 0 1em; padding: 0 1ex;" label="__TBSYNCMSG_manager.help.createbugreport__" oncommand="TbSync.manager.openBugReportWizard();" />
+ <button style="margin:0 0 0 1em; padding: 0 1ex;" label="__TBSYNCMSG_manager.help.viewdebuglog__" oncommand="TbSync.manager.viewDebugLog();" />
+ </hbox>
+
+ <hbox flex="1">
+ </hbox>
+
+ </vbox>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/installProvider.xhtml b/content/manager/installProvider.xhtml
new file mode 100644
index 0000000..9ccdd75
--- /dev/null
+++ b/content/manager/installProvider.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="tbSyncManageProvider.prepInstall();"
+ title="Install additional synchronization provider for TbSync" >
+
+ <hbox flex="1" id="mainframe">
+ <vbox flex="1">
+
+ <hbox>
+ <html:p style="font-weight: bold" id="header"></html:p>
+ </hbox>
+
+ <html:p>
+ __TBSYNCMSG_manager.installprovider.link__
+ </html:p>
+
+ <html:p id="link" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="TbSync.manager.prefWindowObj.tbSyncAccountManager.selectTab(0); TbSync.manager.openTBtab(this.getAttribute('link'));" style="color:blue;text-decoration: underline;padding-left:1em;">
+ </html:p>
+
+ <html:p id="warning" style="font-weight: bold">
+ __TBSYNCMSG_manager.installprovider.warning__
+ </html:p>
+
+ </vbox>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/manageProvider.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/manageProvider.js b/content/manager/manageProvider.js
new file mode 100644
index 0000000..01b3be5
--- /dev/null
+++ b/content/manager/manageProvider.js
@@ -0,0 +1,40 @@
+/*
+ * 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncManageProvider = {
+
+ prepInstall: function () {
+ let url = window.location.toString();
+ let provider = url.split("provider=")[1];
+ window.document.getElementById("header").textContent = TbSync.getString("installProvider.header::" + TbSync.providers.defaultProviders[provider].name);
+
+ window.document.getElementById("link").textContent = TbSync.providers.defaultProviders[provider].homepageUrl;
+ window.document.getElementById("link").setAttribute("link", TbSync.providers.defaultProviders[provider].homepageUrl);
+
+ window.document.getElementById("warning").hidden = TbSync.providers.defaultProviders[provider].homepageUrl.startsWith("https://addons.thunderbird.net");
+ },
+
+ prepMissing: function () {
+ let url = window.location.toString();
+ let provider = url.split("provider=")[1];
+
+ let e = window.document.getElementById("missing");
+ let v = e.textContent;
+ e.textContent = v.replace("##provider##", provider.toUpperCase());
+
+ if (TbSync.providers.defaultProviders.hasOwnProperty(provider)) {
+ window.document.getElementById("link").textContent = TbSync.providers.defaultProviders[provider].homepageUrl;
+ window.document.getElementById("link").setAttribute("link", TbSync.providers.defaultProviders[provider].homepageUrl);
+ }
+
+ },
+};
diff --git a/content/manager/manager.css b/content/manager/manager.css
new file mode 100644
index 0000000..c6359bf
--- /dev/null
+++ b/content/manager/manager.css
@@ -0,0 +1,38 @@
+#manager {
+ margin:8px 12px 12px 12px;
+}
+
+#tbtoolbar {
+ border:1px solid darkgrey;
+ background-color: #ffffff;
+ padding: 0;
+ margin:0 0 12px 0;
+}
+
+#tbtoolbar vbox {
+ margin: 0;
+ padding:6px 6px 2px 6px;
+ background-color: #ffffff;
+}
+
+#tbtoolbar vbox:hover {
+ background-color: #e0e8f6;
+}
+
+#tbtoolbar vbox[active="true"] {
+ background-color: #c1d2ee;
+}
+
+#mainframe {
+ padding:0 12px;
+ margin:0;
+}
+
+.row vbox {
+ width:140px;
+}
+
+iframe {
+ margin:0;
+ padding:0;
+}
diff --git a/content/manager/missingProvider.xhtml b/content/manager/missingProvider.xhtml
new file mode 100644
index 0000000..fbdc26f
--- /dev/null
+++ b/content/manager/missingProvider.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="tbSyncManageProvider.prepMissing();"
+ title="TbSync Account Settings" >
+
+ <hbox flex="1" pack="center">
+ <vbox flex="1" pack="center">
+ <description style="text-align:center" id="missing">__TBSYNCMSG_manager.missingprovider__</description>
+ <html:p id="link" onmouseover="this.style.cursor='pointer'" onmouseout="this.style.cursor='default'" onclick="TbSync.manager.openTBtab(this.getAttribute('link'));" style="color:blue; text-decoration: underline; text-align:center;"></html:p>
+ </vbox>
+
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/manageProvider.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/noaccounts.xhtml b/content/manager/noaccounts.xhtml
new file mode 100644
index 0000000..2437218
--- /dev/null
+++ b/content/manager/noaccounts.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="TbSync Account Settings" >
+
+ <hbox flex="1" pack="center">
+ <vbox flex="1" pack="center">
+ <description style="text-align:center">__TBSYNCMSG_manager.noaccounts__</description>
+ </vbox>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/manager/support-wizard/support-wizard.xhtml b/content/manager/support-wizard/support-wizard.xhtml
new file mode 100644
index 0000000..ce7ea27
--- /dev/null
+++ b/content/manager/support-wizard/support-wizard.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ width="500"
+ height="600"
+ onload="tbSyncAccountManager.initSupportWizard();"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ </linkset>
+
+ <wizard
+ id="SupportWizard"
+ title="__TBSYNCMSG_supportwizard.title__">
+
+ <wizardpage onFirstPage="true" label="__TBSYNCMSG_supportwizard.pagetitle__">
+ <label value="__TBSYNCMSG_supportwizard.label.faultycomponent__"/>
+ <menulist id="tbsync.supportwizard.faultycomponent.menulist">
+ <menupopup id="tbsync.supportwizard.faultycomponent">
+ <menuitem hidden="true" selected="true" label="__TBSYNCMSG_supportwizard.label.selectcomponent__" value="" />
+ <menuitem label="__TBSYNCMSG_manager.title__" value="core" />
+ </menupopup>
+ </menulist>
+
+ <label style="margin-top:1em" value="__TBSYNCMSG_supportwizard.label.summary__"/>
+ <html:input id="tbsync.supportwizard.summary" oninput="tbSyncAccountManager.checkSupportWizard()" />
+
+ <label style="margin-top:1em" value="__TBSYNCMSG_supportwizard.label.description__"/>
+ <html:textarea rows="15" style="margin-left: 1ex;" id="tbsync.supportwizard.description" />
+
+ <description style="margin-top:1em">
+ __TBSYNCMSG_supportwizard.footer__
+ </description>
+ </wizardpage>
+
+ </wizard>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+
+</window>
diff --git a/content/manager/supporter.xhtml b/content/manager/supporter.xhtml
new file mode 100644
index 0000000..a9bee40
--- /dev/null
+++ b/content/manager/supporter.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/manager/manager.css" type="text/css"?>
+
+<window
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="tbSyncAccountManager.initCommunity();"
+ title="Community" >
+
+ <hbox flex="1" id="mainframe">
+ <vbox flex="1">
+ <html:p>
+ <html:b>__TBSYNCMSG_manager.supporter.contributors__</html:b>
+ <html:div id="listOfContributors" style="margin-top:10px">
+
+ <html:div provider="" style="width:225px; margin: 0 0 10px 0; float:left;">
+ <html:img
+ src=""
+ style="width:48px; height:48px; margin:4px 0 0 10px; float:left"
+ onclick="TbSync.manager.openLink(TbSync.providers.loadedProviders[this.parentNode.getAttribute('provider')].addon.homepageURL);"
+ onmouseover="this.style.cursor='pointer'"
+ onmouseout="this.style.cursor='default'" />
+ <html:div style="padding-left: 65px;">
+ <html:div>__TBSYNCMSG_manager.title__</html:div>
+ <html:div style="font-style:italic">__TBSYNCMSG_manager.provider4tbsync__</html:div>
+ <html:div style="font-style:italic;">[<html:span
+ onclick="TbSync.manager.openLink(TbSync.providers.loadedProviders[this.parentNode.parentNode.parentNode.getAttribute('provider')].addon.contributorsURL);"
+ onmouseover="this.style.cursor='pointer'"
+ onmouseout="this.style.cursor='default'"
+ style="color:blue;text-decoration: underline;">__TBSYNCMSG_manager.supporter.details__</html:span>]</html:div>
+ </html:div>
+ </html:div>
+
+ <html:div provider="" style="width:225px; margin: 0 0 10px 0; float:left;">
+ <html:img
+ src="chrome://tbsync/content/skin/tbsync64.png"
+ style="width:48px; height:48px; margin:4px 0 0 10px; float:left"
+ onclick="TbSync.manager.openLink(TbSync.addon.homepageURL);"
+ onmouseover="this.style.cursor='pointer'"
+ onmouseout="this.style.cursor='default'" />
+ <html:div style="padding-left: 65px;">
+ <html:span></html:span>
+ <html:div>TbSync</html:div>
+ <html:div style="font-style:italic">__TBSYNCMSG_manager.shorttitle__</html:div>
+ <html:div style="font-style:italic;">[<html:span
+ onclick="TbSync.manager.openLink(TbSync.addon.contributorsURL);"
+ onmouseover="this.style.cursor='pointer'"
+ onmouseout="this.style.cursor='default'"
+ style="color:blue;text-decoration: underline;">__TBSYNCMSG_manager.supporter.details__</html:span>]</html:div>
+ </html:div>
+ </html:div>
+
+ </html:div>
+ </html:p>
+
+
+ <html:p>
+ <html:b>__TBSYNCMSG_manager.supporter.sponsors__</html:b>
+ <html:div id="listOfSponsors" style="margin-top:10px">
+ <html:div link="" style="width:225px; margin: 0 0 10px 0; float:left;">
+ <html:img src="chrome://tbsync/content/skin/user48.png" style="width:48px; height:48px; padding:0px; margin:0 0 0 10px; border:1px solid #d3d3d3; float:left" onclick="if (this.parentNode.getAttribute('link') != '') {TbSync.manager.openLink(this.parentNode.getAttribute('link'));}" onmouseover="if (this.parentNode.getAttribute('link') != '') {this.style.cursor='pointer'}" onmouseout="this.style.cursor='default'" />
+ <html:div style="padding-left: 65px;">
+ <html:div >Name</html:div>
+ <html:div style="font-style:italic">Description</html:div>
+ </html:div>
+ </html:div>
+ </html:div>
+ </html:p>
+
+ </vbox>
+ </hbox>
+
+ <script type="text/javascript" src="chrome://tbsync/content/manager/accountManager.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+</window>
diff --git a/content/modules/addressbook.js b/content/modules/addressbook.js
new file mode 100644
index 0000000..d239a95
--- /dev/null
+++ b/content/modules/addressbook.js
@@ -0,0 +1,1149 @@
+/*
+ * 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 { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddrBookCard: "resource:///modules/AddrBookCard.jsm"
+});
+
+var addressbook = {
+
+ _notifications: [
+ "addrbook-directory-updated",
+ "addrbook-directory-deleted",
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-deleted",
+ "addrbook-list-member-added",
+ "addrbook-list-member-removed",
+ "addrbook-list-deleted",
+ "addrbook-list-updated",
+ "addrbook-list-created"
+ ],
+
+ load : async function () {
+ for (let topic of this._notifications) {
+ Services.obs.addObserver(this.addressbookObserver, topic);
+ }
+ },
+
+ unload : async function () {
+ for (let topic of this._notifications) {
+ Services.obs.removeObserver(this.addressbookObserver, topic);
+ }
+ },
+
+ getStringValue : function (ab, value, fallback) {
+ try {
+ return ab.getStringValue(value, fallback);
+ } catch (e) {
+ return fallback;
+ }
+ },
+
+ searchDirectory: function (uri, search) {
+ return new Promise((resolve, reject) => {
+ let listener = {
+ cards : [],
+
+ onSearchFinished(aResult, aErrorMsg) {
+ resolve(this.cards);
+ },
+ onSearchFoundCard(aCard) {
+ this.cards.push(aCard.QueryInterface(Components.interfaces.nsIAbCard));
+ }
+ }
+
+ let result = MailServices.ab.getDirectory(uri).search(search, "", listener);
+ });
+ },
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * AdvancedTargetData, an extended TargetData implementation, providers
+ // * can use this as their own TargetData by extending it and just
+ // * defining the extra methods
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ AdvancedTargetData : class {
+ constructor(folderData) {
+ this._folderData = folderData;
+ this._targetObj = null;
+ }
+
+
+ // Check, if the target exists and return true/false.
+ hasTarget() {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+ return directory ? true : false;
+ }
+
+ // Returns the target obj, which TbSync should return as the target. It can
+ // be whatever you want and is returned by FolderData.targetData.getTarget().
+ // If the target does not exist, it should be created. Throw a simple Error, if that
+ // failed.
+ async getTarget() {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+
+ if (!directory) {
+ // create a new addressbook and store its UID in folderData
+ directory = await TbSync.addressbook.prepareAndCreateAddressbook(this._folderData);
+ if (!directory)
+ throw new Error("notargets");
+ }
+
+ if (!this._targetObj || this._targetObj.UID != directory.UID)
+ this._targetObj = new TbSync.addressbook.AbDirectory(directory, this._folderData);
+
+ return this._targetObj;
+ }
+
+ /**
+ * Removes the target from the local storage. If it does not exist, return
+ * silently. A call to ``hasTarget()`` should return false, after this has
+ * been executed.
+ *
+ */
+ removeTarget() {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+ try {
+ if (directory) {
+ MailServices.ab.deleteAddressBook(directory.URI);
+ }
+ } catch (e) {}
+
+ TbSync.db.clearChangeLog(target);
+ this._folderData.resetFolderProperty("target");
+ }
+
+ /**
+ * Disconnects the target in the local storage from this TargetData, but
+ * does not delete it, so it becomes a stale "left over" . A call
+ * to ``hasTarget()`` should return false, after this has been executed.
+ *
+ */
+ disconnectTarget() {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+ if (directory) {
+ let changes = TbSync.db.getItemsFromChangeLog(target, 0, "_by_user");
+ if (changes.length > 0) {
+ this.targetName = this.targetName + " (*)";
+ }
+ directory.setStringValue("tbSyncIcon", "orphaned");
+ directory.setStringValue("tbSyncProvider", "orphaned");
+ directory.setStringValue("tbSyncAccountID", "");
+ }
+ TbSync.db.clearChangeLog(target);
+ this._folderData.resetFolderProperty("target");
+ }
+
+ set targetName(newName) {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+ if (directory) {
+ directory.dirName = newName;
+ } else {
+ throw new Error("notargets");
+ }
+ }
+
+ get targetName() {
+ let target = this._folderData.getFolderProperty("target");
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(target);
+ if (directory) {
+ return directory.dirName;
+ } else {
+ throw new Error("notargets");
+ }
+ }
+
+ setReadOnly(value) {
+ }
+
+
+ // * * * * * * * * * * * * * * * * *
+ // * AdvancedTargetData extension *
+ // * * * * * * * * * * * * * * * * *
+
+ get isAdvancedAddressbookTargetData() {
+ return true;
+ }
+
+ get folderData() {
+ return this._folderData;
+ }
+
+ // define a card property, which should be used for the changelog
+ // basically your primary key for the abItem properties
+ // UID will be used, if nothing specified
+ get primaryKeyField() {
+ return "UID";
+ }
+
+ generatePrimaryKey() {
+ return TbSync.generateUUID();
+ }
+
+ // enable or disable changelog
+ get logUserChanges() {
+ return true;
+ }
+
+ directoryObserver(aTopic) {
+ switch (aTopic) {
+ case "addrbook-directory-deleted":
+ case "addrbook-directory-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] " + folderData.getFolderProperty("foldername"));
+ break;
+ }
+ }
+
+ cardObserver(aTopic, abCardItem) {
+ switch (aTopic) {
+ case "addrbook-contact-updated":
+ case "addrbook-contact-deleted":
+ case "addrbook-contact-created":
+ //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName"));
+ break;
+ }
+ }
+
+ listObserver(aTopic, abListItem, abListMember) {
+ switch (aTopic) {
+ case "addrbook-list-member-added":
+ case "addrbook-list-member-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-list-deleted":
+ case "addrbook-list-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName"));
+ break;
+
+ case "addrbook-list-created":
+ //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for List <"+abListItem.getProperty("ListName")+">");
+ break;
+ }
+ }
+
+ // replace this with your own implementation to create the actual addressbook,
+ // when this class is extended
+ async createAddressbook(newname) {
+ // https://searchfox.org/comm-central/source/mailnews/addrbook/src/nsDirPrefs.h
+ let dirPrefId = MailServices.ab.newAddressBook(newname, "", 101);
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+ return directory;
+ }
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * AbItem and AbDirectory Classes
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ AbItem : class {
+ constructor(abDirectory, item) {
+ if (!abDirectory)
+ throw new Error("AbItem::constructor is missing its first parameter!");
+
+ if (!item)
+ throw new Error("AbItem::constructor is missing its second parameter!");
+
+ this._abDirectory = abDirectory;
+ this._card = null;
+ this._tempListDirectory = null;
+ this._tempProperties = null;
+ this._isMailList = false;
+
+ if (item instanceof Components.interfaces.nsIAbDirectory) {
+ this._tempListDirectory = item;
+ this._isMailList = true;
+ this._tempProperties = {};
+ } else {
+ this._card = item;
+ this._isMailList = item.isMailList;
+ }
+ }
+
+ get abDirectory() {
+ return this._abDirectory;
+ }
+
+ get isMailList() {
+ return this._isMailList;
+ }
+
+
+
+
+
+ get nativeItem() {
+ return this._card;
+ }
+
+ get UID() {
+ if (this._tempListDirectory) return this._tempListDirectory.UID;
+ return this._card.UID;
+ }
+
+ get primaryKey() {
+ //use UID as fallback
+ let key = this._abDirectory.primaryKeyField;
+ return key ? this.getProperty(key) : this.UID;
+ }
+
+ set primaryKey(value) {
+ //use UID as fallback
+ let key = this._abDirectory.primaryKeyField;
+ if (key) this.setProperty(key, value)
+ else throw ("TbSync.addressbook.AbItem.set primaryKey: UID is used as primaryKeyField but changing the UID of an item is currently not supported. Please use a custom primaryKeyField.");
+ }
+
+ clone() { //no real clone ... this is just here to match the calendar target
+ return new TbSync.addressbook.AbItem(this._abDirectory, this._card);
+ }
+
+ toString() {
+ return this._card.displayName + " (" + this._card.firstName + ", " + this._card.lastName + ") <"+this._card.primaryEmail+">";
+ }
+
+ // mailinglist aware method to get properties of cards
+ // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog
+ getProperty(property, fallback = "") {
+ if (property == "UID")
+ return this.UID;
+
+ if (this._isMailList) {
+ const directListProperties = {
+ ListName: "dirName",
+ ListNickName: "listNickName",
+ ListDescription: "description"
+ };
+
+ let value;
+ if (directListProperties.hasOwnProperty(property)) {
+ try {
+ let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI); //this._card.asDirectory
+ value = mailListDirectory[directListProperties[property]];
+ } catch (e) {
+ // list does not exists
+ }
+ } else {
+ value = this._tempProperties ? this._tempProperties[property] : TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID + "#" + this.UID, property);
+ }
+ return value || fallback;
+ } else {
+ return this._card.getProperty(property, fallback);
+ }
+ }
+
+ // mailinglist aware method to set properties of cards
+ // mailinglist properties cannot be stored in mailinglists themselves, so we store them in changelog
+ // while the list has not been added, we keep all props in an object (UID changes on adding)
+ setProperty(property, value) {
+ // UID cannot be changed (currently)
+ if (property == "UID") {
+ throw ("TbSync.addressbook.AbItem.setProperty: UID cannot be changed currently.");
+ return;
+ }
+
+ if (this._isMailList) {
+ const directListProperties = {
+ ListName: "dirName",
+ ListNickName: "listNickName",
+ ListDescription: "description"
+ };
+
+ if (directListProperties.hasOwnProperty(property)) {
+ try {
+ let mailListDirectory = this._tempListDirectory || MailServices.ab.getDirectory(this._card.mailListURI);
+ mailListDirectory[directListProperties[property]] = value;
+ } catch (e) {
+ // list does not exists
+ }
+ } else {
+ if (this._tempProperties) {
+ this._tempProperties[property] = value;
+ } else {
+ TbSync.db.addItemToChangeLog(this._abDirectory.UID + "#" + this.UID, property, value);
+ }
+ }
+ } else {
+ this._card.setProperty(property, value);
+ }
+ }
+
+ deleteProperty(property) {
+ if (this._isMailList) {
+ if (this._tempProperties) {
+ delete this._tempProperties[property];
+ } else {
+ TbSync.db.removeItemFromChangeLog(this._abDirectory.UID + "#" + this.UID, property);
+ }
+ } else {
+ this._card.deleteProperty(property);
+ }
+ }
+
+ get changelogData() {
+ return TbSync.db.getItemDataFromChangeLog(this._abDirectory.UID, this.primaryKey);
+ }
+
+ get changelogStatus() {
+ return TbSync.db.getItemStatusFromChangeLog(this._abDirectory.UID, this.primaryKey);
+ }
+
+ set changelogStatus(status) {
+ let value = this.primaryKey;
+
+ if (value) {
+ if (!status) {
+ TbSync.db.removeItemFromChangeLog(this._abDirectory.UID, value);
+ return;
+ }
+
+ if (this._abDirectory.logUserChanges || status.endsWith("_by_server")) {
+ TbSync.db.addItemToChangeLog(this._abDirectory.UID, value, status);
+ }
+ }
+ }
+
+
+
+
+
+ // get the property given from all members and return it as an array (that property better be uniqe)
+ getMembersPropertyList(property) {
+ let members = [];
+ if (this._card && this._card.isMailList) {
+ // get mailListDirectory
+ let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI);
+ for (let member of mailListDirectory.childCards) {
+ let prop = member.getProperty(property, "");
+ if (prop) members.push(prop);
+ }
+ }
+ return members;
+ }
+
+ addListMembers(property, candidates) {
+ if (this._card && this._card.isMailList) {
+ let members = this.getMembersPropertyList(property);
+ let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI);
+
+ for (let candidate of candidates) {
+ if (members.includes(candidate))
+ continue;
+
+ let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true);
+ if (card) mailListDirectory.addCard(card);
+ }
+ }
+ }
+
+ removeListMembers(property, candidates) {
+ if (this._card && this._card.isMailList) {
+ let members = this.getMembersPropertyList(property);
+ let mailListDirectory = MailServices.ab.getDirectory(this._card.mailListURI);
+
+ let cardsToRemove = [];
+ for (let candidate of candidates) {
+ if (!members.includes(candidate))
+ continue;
+
+ let card = this._abDirectory._directory.getCardFromProperty(property, candidate, true);
+ if (card) cardsToRemove.push(card);
+ }
+ if (cardsToRemove.length > 0) mailListDirectory.deleteCards(cardsToRemove);
+ }
+ }
+
+ addPhoto(photo, data, extension = "jpg", url = "") {
+ let dest = [];
+ let card = this._card;
+ let bookUID = this.abDirectory.UID;
+
+ // TbSync storage must be set as last
+ let book64 = btoa(bookUID);
+ let photo64 = btoa(photo);
+ let photoName64 = book64 + "_" + photo64 + "." + extension;
+
+ dest.push(["Photos", photoName64]);
+ // I no longer see a reason for this
+ // dest.push(["TbSync","Photos", book64, photo64]);
+
+ let filePath = "";
+ for (let i=0; i < dest.length; i++) {
+ let file = FileUtils.getFile("ProfD", dest[i]);
+
+ let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
+ foStream.init(file, 0x02 | 0x08 | 0x20, 0x180, 0); // write, create, truncate
+ let binary = "";
+ try {
+ binary = atob(data.split(" ").join(""));
+ } catch (e) {
+ console.log("Failed to decode base64 string:", data);
+ }
+ foStream.write(binary, binary.length);
+ foStream.close();
+
+ filePath = 'file:///' + file.path.replace(/\\/g, '\/').replace(/^\s*\/?/, '').replace(/\ /g, '%20');
+ }
+ card.setProperty("PhotoName", photoName64);
+ card.setProperty("PhotoType", url ? "web" : "file");
+ card.setProperty("PhotoURI", url ? url : filePath);
+ return filePath;
+ }
+
+ getPhoto() {
+ let card = this._card;
+ let photo = card.getProperty("PhotoName", "");
+ let data = "";
+
+ if (photo) {
+ try {
+ let file = FileUtils.getFile("ProfD", ["Photos", photo]);
+
+ let fiStream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
+ fiStream.init(file, -1, -1, false);
+
+ let bstream = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
+ bstream.setInputStream(fiStream);
+
+ data = btoa(bstream.readBytes(bstream.available()));
+ fiStream.close();
+ } catch (e) {}
+ }
+ return data;
+ }
+ },
+
+ AbDirectory : class {
+ constructor(directory, folderData) {
+ this._directory = directory;
+ this._folderData = folderData;
+ }
+
+ get directory() {
+ return this._directory;
+ }
+
+ get logUserChanges() {
+ return this._folderData.targetData.logUserChanges;
+ }
+
+ get primaryKeyField() {
+ return this._folderData.targetData.primaryKeyField;
+ }
+
+ get UID() {
+ return this._directory.UID;
+ }
+
+ get URI() {
+ return this._directory.URI;
+ }
+
+ createNewCard() {
+ let card = new AddrBookCard();
+ return new TbSync.addressbook.AbItem(this, card);
+ }
+
+ createNewList() {
+ let listDirectory = Components.classes["@mozilla.org/addressbook/directoryproperty;1"].createInstance(Components.interfaces.nsIAbDirectory);
+ listDirectory.isMailList = true;
+ return new TbSync.addressbook.AbItem(this, listDirectory);
+ }
+
+ async addItem(abItem, pretagChangelogWithByServerEntry = true) {
+ if (this.primaryKeyField && !abItem.getProperty(this.primaryKeyField)) {
+ abItem.setProperty(this.primaryKeyField, this._folderData.targetData.generatePrimaryKey());
+ //Services.console.logStringMessage("[AbDirectory::addItem] Generated primary key!");
+ }
+
+ if (pretagChangelogWithByServerEntry) {
+ abItem.changelogStatus = "added_by_server";
+ }
+
+ if (abItem.isMailList && abItem._tempListDirectory) {
+ let list = this._directory.addMailList(abItem._tempListDirectory);
+ // the list has been added and we can now get the corresponding card via its UID
+ let found = await this.getItemFromProperty("UID", list.UID);
+
+ // clone and clear temporary properties
+ let props = {...abItem._tempProperties};
+ abItem._tempListDirectory = null;
+ abItem._tempProperties = null;
+
+ // store temporary properties
+ for (const [property, value] of Object.entries(props)) {
+ found.setProperty(property, value);
+ }
+
+ abItem._card = found._card;
+ } else if (!abItem.isMailList) {
+ this._directory.addCard(abItem._card);
+
+ } else {
+ throw new Error("Cannot re-add a list to a directory.");
+ }
+ }
+
+ modifyItem(abItem, pretagChangelogWithByServerEntry = true) {
+ // only add entry if the current entry does not start with _by_user
+ let status = abItem.changelogStatus ? abItem.changelogStatus : "";
+ if (pretagChangelogWithByServerEntry && !status.endsWith("_by_user")) {
+ abItem.changelogStatus = "modified_by_server";
+ }
+
+ if (abItem.isMailList) {
+ // get mailListDirectory
+ let mailListDirectory = MailServices.ab.getDirectory(abItem._card.mailListURI);
+
+ // store
+ mailListDirectory.editMailListToDatabase(abItem._card);
+ } else {
+ this._directory.modifyCard(abItem._card);
+ }
+ }
+
+ deleteItem(abItem, pretagChangelogWithByServerEntry = true) {
+ if (pretagChangelogWithByServerEntry) {
+ abItem.changelogStatus = "deleted_by_server";
+ }
+ this._directory.deleteCards([abItem._card]);
+ }
+
+ async getItem(searchId) {
+ //use UID as fallback
+ let key = this.primaryKeyField ? this.primaryKeyField : "UID";
+ return await this.getItemFromProperty(key, searchId);
+ }
+
+ async getItemFromProperty(property, value) {
+ // try to use the standard card method first
+ let card = this._directory.getCardFromProperty(property, value, true);
+ if (card) {
+ return new TbSync.addressbook.AbItem(this, card);
+ }
+
+ // search for list cards
+ // we cannot search for the prop directly, because for mailinglists
+ // they are not part of the card (expect UID) but stored in a custom storage
+ let searchList = "(IsMailList,=,TRUE)";
+ let foundCards = await TbSync.addressbook.searchDirectory(this._directory.URI, "(or" + searchList+")");
+ for (let aCard of foundCards) {
+ let card = new TbSync.addressbook.AbItem(this, aCard);
+ //does this list card have the req prop?
+ if (card.getProperty(property) == value) {
+ return card;
+ }
+ }
+ return null;
+ }
+
+ getAllItems () {
+ let rv = [];
+ for (let card of this._directory.childCards) {
+ rv.push(new TbSync.addressbook.AbItem( this._directory, card ));
+ }
+ return rv;
+ }
+
+
+
+
+
+ getAddedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "added_by_user").map(item => item.itemId);
+ }
+
+ getModifiedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "modified_by_user").map(item => item.itemId);
+ }
+
+ getDeletedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "deleted_by_user").map(item => item.itemId);
+ }
+
+ getItemsFromChangeLog(maxitems = 0) { // Document what this returns
+ return TbSync.db.getItemsFromChangeLog(this._directory.UID, maxitems, "_by_user");
+ }
+
+ removeItemFromChangeLog(id, moveToEndInsteadOfDelete = false) {
+ TbSync.db.removeItemFromChangeLog(this._directory.UID, id, moveToEndInsteadOfDelete);
+ }
+
+ clearChangelog() {
+ TbSync.db.clearChangeLog(this._directory.UID);
+ }
+
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * Internal Functions
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ prepareAndCreateAddressbook: async function (folderData) {
+ let target = folderData.getFolderProperty("target");
+ let provider = folderData.accountData.getAccountProperty("provider");
+
+ // Get cached or new unique name for new address book
+ let cachedName = folderData.getFolderProperty("targetName");
+ let newname = cachedName == "" ? folderData.accountData.getAccountProperty("accountname") + " (" + folderData.getFolderProperty("foldername")+ ")" : cachedName;
+
+ //Create the new book with the unique name
+ let directory = await folderData.targetData.createAddressbook(newname);
+ if (directory && directory instanceof Components.interfaces.nsIAbDirectory) {
+ directory.setStringValue("tbSyncProvider", provider);
+ directory.setStringValue("tbSyncAccountID", folderData.accountData.accountID);
+
+ // Prevent gContactSync to inject its stuff into New/EditCard dialogs
+ // https://github.com/jdgeenen/gcontactsync/pull/127
+ directory.setStringValue("gContactSyncSkipped", "true");
+
+ folderData.setFolderProperty("target", directory.UID);
+ folderData.setFolderProperty("targetName", directory.dirName);
+ //notify about new created address book
+ Services.obs.notifyObservers(null, 'tbsync.observer.addressbook.created', null)
+ return directory;
+ }
+
+ return null;
+ },
+
+ getFolderFromDirectoryUID: function(bookUID) {
+ let folders = TbSync.db.findFolders({"target": bookUID});
+ if (folders.length == 1) {
+ let accountData = new TbSync.AccountData(folders[0].accountID);
+ return new TbSync.FolderData(accountData, folders[0].folderID);
+ }
+ return null;
+ },
+
+ getDirectoryFromDirectoryUID: function(UID) {
+ if (!UID)
+ return null;
+
+ for (let directory of MailServices.ab.directories) {
+ if (directory instanceof Components.interfaces.nsIAbDirectory) {
+ if (directory.UID == UID) return directory;
+ }
+ }
+ return null;
+ },
+
+ getListInfoFromListUID: async function(UID) {
+ for (let directory of MailServices.ab.directories) {
+ if (directory instanceof Components.interfaces.nsIAbDirectory && !directory.isRemote) {
+ let searchList = "(IsMailList,=,TRUE)";
+ let foundCards = await TbSync.addressbook.searchDirectory(directory.URI, "(and" + searchList+")");
+ for (let listCard of foundCards) {
+ //return after first found card
+ if (listCard.UID == UID) return {directory, listCard};
+ }
+ }
+ }
+ throw new Error("List with UID <" + UID + "> does not exists");
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * Addressbook Observer and Listener
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ addressbookObserver: {
+ observe: async function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ // we do not need addrbook-created
+ case "addrbook-directory-updated":
+ case "addrbook-directory-deleted":
+ {
+ //aSubject: nsIAbDirectory (we can get URI and UID directly from the object, but the directory no longer exists)
+ aSubject.QueryInterface(Components.interfaces.nsIAbDirectory);
+ let bookUID = aSubject.UID;
+
+ let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedAddressbookTargetData) {
+
+ switch(aTopic) {
+ case "addrbook-directory-updated":
+ {
+ //update name of target (if changed)
+ folderData.setFolderProperty("targetName", aSubject.dirName);
+ //update settings window, if open
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ }
+ break;
+
+ case "addrbook-directory-deleted":
+ {
+ //delete any pending changelog of the deleted book
+ TbSync.db.clearChangeLog(bookUID);
+
+ //unselect book if deleted by user and update settings window, if open
+ if (folderData.getFolderProperty("selected")) {
+ folderData.setFolderProperty("selected", false);
+ //update settings window, if open
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ }
+
+ folderData.resetFolderProperty("target");
+ }
+ break;
+ }
+
+ folderData.targetData.directoryObserver(aTopic);
+ }
+ }
+ break;
+
+ case "addrbook-contact-created":
+ case "addrbook-contact-updated":
+ case "addrbook-contact-deleted":
+ {
+ //aSubject: nsIAbCard
+ aSubject.QueryInterface(Components.interfaces.nsIAbCard);
+ //aData: 128-bit unique identifier for the parent directory
+ let bookUID = aData;
+
+ let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedAddressbookTargetData) {
+
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID);
+ let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData);
+ let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject);
+ let itemStatus = abItem.changelogStatus || "";
+
+ // during create the following can happen
+ // card has no primary key
+ // another process could try to mod
+ // -> we need to identify this card with an always available ID and block any other MODS until we free it again
+ // -> store creation type
+
+ if (aTopic == "addrbook-contact-created" && itemStatus == "") {
+ // add this new card to changelog to keep track of it
+ TbSync.db.addItemToChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation", Date.now());
+ // new cards must get a NEW(!) primaryKey first
+ if (abDirectory.primaryKeyField) {
+ console.log("New primary Key generated!");
+ abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey());
+ }
+ // special case: do not add "modified_by_server"
+ abDirectory.modifyItem(abItem, /*pretagChangelogWithByServerEntry */ false);
+ // We will see this card again as updated but delayed created
+ return;
+ }
+
+ // during follow up MODs we can identify this card via
+ let delayedUserCreation = TbSync.db.getItemStatusFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation");
+
+ // if we reach this point and if we have adelayedUserCreation,
+ // we can remove the delayedUserCreation marker and can
+ // continue to process this event as an addrbook-contact-created
+ let bTopic = aTopic;
+ if (delayedUserCreation) {
+ let age = Date.now() - delayedUserCreation;
+ if (age < 1500) {
+ bTopic = "addrbook-contact-created";
+ } else {
+ TbSync.db.removeItemFromChangeLog(bookUID, aSubject.UID + "#DelayedUserCreation");
+ }
+ }
+
+ // if this card was created by us, it will be in the log
+ // we want to ignore any MOD for a freeze time, because
+ // gContactSync modifies our(!) contacts (GoogleID) after we added them, so they get
+ // turned into "modified_by_user" and will be send back to the server.
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ let age = Date.now() - abItem.changelogData.timestamp;
+ if (age < 1500) {
+ // during freeze, local modifications are not possible
+ return;
+ } else {
+ // remove blocking entry from changelog after freeze time is over (1.5s),
+ // and continue evaluating this event
+ abItem.changelogStatus = "";
+ }
+ }
+
+ // From here on, we only process user changes as server changes are self freezed
+ // update changelog based on old status
+ switch (bTopic) {
+ case "addrbook-contact-created":
+ {
+ switch (itemStatus) {
+ case "added_by_user":
+ // late create notification
+ break;
+
+ case "modified_by_user":
+ // late create notification
+ abItem.changelogStatus = "added_by_user";
+ break;
+
+ case "deleted_by_user":
+ // unprocessed delete for this card, undo the delete (moved out and back in)
+ abItem.changelogStatus = "modified_by_user";
+ break;
+
+ default:
+ // new card
+ abItem.changelogStatus = "added_by_user";
+ }
+ }
+ break;
+
+ case "addrbook-contact-updated":
+ {
+ switch (itemStatus) {
+ case "added_by_user":
+ // unprocessed add for this card, keep status
+ break;
+
+ case "modified_by_user":
+ // double notification, keep status
+ break;
+
+ case "deleted_by_user":
+ // race? unprocessed delete for this card, moved out and back in and modified
+ default:
+ abItem.changelogStatus = "modified_by_user";
+ break;
+ }
+ }
+ break;
+
+ case "addrbook-contact-deleted":
+ {
+ switch (itemStatus) {
+ case "added_by_user":
+ // unprocessed add for this card, revert
+ abItem.changelogStatus = "";
+ return;
+
+ case "deleted_by_user":
+ // double notification
+ break;
+
+ case "modified_by_user":
+ // unprocessed mod for this card
+ default:
+ abItem.changelogStatus = "deleted_by_user";
+ break;
+ }
+ }
+ break;
+ }
+
+ if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData);
+
+ // notify observers only if status changed
+ if (itemStatus != abItem.changelogStatus) {
+ folderData.targetData.cardObserver(bTopic, abItem);
+ }
+ return;
+ }
+ }
+ break;
+
+ case "addrbook-list-created":
+ case "addrbook-list-deleted":
+ {
+ //aSubject: nsIAbDirectory
+ aSubject.QueryInterface(Components.interfaces.nsIAbDirectory);
+ //aData: 128-bit unique identifier for the parent directory
+ let bookUID = aData;
+
+ let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedAddressbookTargetData) {
+
+ let directory = TbSync.addressbook.getDirectoryFromDirectoryUID(bookUID);
+ let abDirectory = new TbSync.addressbook.AbDirectory(directory, folderData);
+ let abItem = new TbSync.addressbook.AbItem(abDirectory, aSubject);
+
+ let itemStatus = abItem.changelogStatus;
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ //we caused this, ignore
+ abItem.changelogStatus = "";
+ return;
+ }
+
+ // update changelog based on old status
+ switch (aTopic) {
+ case "addrbook-list-created":
+ {
+ if (abDirectory.primaryKeyField) {
+ // Since we do not need to update a list, to make custom properties persistent, we do not need to use delayedUserCreation as with contacts.
+ abItem.setProperty(abDirectory.primaryKeyField, folderData.targetData.generatePrimaryKey());
+ }
+
+ switch (itemStatus) {
+ case "added_by_user":
+ // double notification, which is probably impossible, keep status
+ break;
+
+ case "modified_by_user":
+ // late create notification
+ abItem.changelogStatus = "added_by_user";
+ break;
+
+ case "deleted_by_user":
+ // unprocessed delete for this card, undo the delete (moved out and back in)
+ abItem.changelogStatus = "modified_by_user";
+ break;
+
+ default:
+ // new list
+ abItem.changelogStatus = "added_by_user";
+ break;
+ }
+ }
+ break;
+
+ case "addrbook-list-deleted":
+ {
+ switch (itemStatus) {
+ case "added_by_user":
+ // unprocessed add for this card, revert
+ abItem.changelogStatus = "";
+ return;
+
+ case "modified_by_user":
+ // unprocessed mod for this card
+ case "deleted_by_user":
+ // double notification
+ default:
+ abItem.changelogStatus = "deleted_by_user";
+ break;
+ }
+ //remove properties of this ML stored in changelog
+ TbSync.db.clearChangeLog(abDirectory.UID + "#" + abItem.UID);
+ }
+ break;
+ }
+
+ if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.listObserver(aTopic, abItem, null);
+ }
+ }
+ break;
+
+ case "addrbook-list-updated":
+ {
+ // aSubject: nsIAbDirectory
+ aSubject.QueryInterface(Components.interfaces.nsIAbDirectory);
+ // get the card representation of this list, including its parent directory
+ let listInfo = await TbSync.addressbook.getListInfoFromListUID(aSubject.UID);
+ let bookUID = listInfo.directory.UID;
+
+ let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedAddressbookTargetData) {
+
+ let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData);
+ let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard);
+
+ let itemStatus = abItem.changelogStatus;
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ //we caused this, ignore
+ abItem.changelogStatus = "";
+ return;
+ }
+
+ // update changelog based on old status
+ switch (aTopic) {
+ case "addrbook-list-updated":
+ {
+ switch (itemStatus) {
+ case "added_by_user":
+ // unprocessed add for this card, keep status
+ break;
+
+ case "modified_by_user":
+ // double notification, keep status
+ break;
+
+ case "deleted_by_user":
+ // race? unprocessed delete for this card, moved out and back in and modified
+ default:
+ abItem.changelogStatus = "modified_by_user";
+ break;
+ }
+ }
+ break;
+ }
+
+ if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.listObserver(aTopic, abItem, null);
+ }
+ }
+ break;
+
+ // unknown, if called for programmatically added members as well, probably not
+ case "addrbook-list-member-added": //exclude contact without Email - notification is wrongly send
+ case "addrbook-list-member-removed":
+ {
+ //aSubject: nsIAbCard of Member
+ aSubject.QueryInterface(Components.interfaces.nsIAbCard);
+ //aData: 128-bit unique identifier for the list
+ let listInfo = await TbSync.addressbook.getListInfoFromListUID(aData);
+ let bookUID = listInfo.directory.UID;
+
+ let folderData = TbSync.addressbook.getFolderFromDirectoryUID(bookUID);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedAddressbookTargetData) {
+
+ let abDirectory = new TbSync.addressbook.AbDirectory(listInfo.directory, folderData);
+ let abItem = new TbSync.addressbook.AbItem(abDirectory, listInfo.listCard);
+ let abMember = new TbSync.addressbook.AbItem(abDirectory, aSubject);
+
+ if (abDirectory.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.listObserver(aTopic, abItem, abMember);
+
+ // removed, added members cause the list to be changed
+ let mailListDirectory = MailServices.ab.getDirectory(listInfo.listCard.mailListURI);
+ TbSync.addressbook.addressbookObserver.observe(mailListDirectory, "addrbook-list-updated", null);
+ return;
+ }
+ }
+ break;
+
+ }
+ }
+ },
+
+}
diff --git a/content/modules/core.js b/content/modules/core.js
new file mode 100644
index 0000000..6a82af3
--- /dev/null
+++ b/content/modules/core.js
@@ -0,0 +1,332 @@
+/*
+ * 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 core = {
+
+ syncDataObj : null,
+
+ load: async function () {
+ this.syncDataObj = {};
+ },
+
+ unload: async function () {
+ },
+
+ isSyncing: function (accountID) {
+ let status = TbSync.db.getAccountProperty(accountID, "status"); //global status of the account
+ return (status == "syncing");
+ },
+
+ isEnabled: function (accountID) {
+ let status = TbSync.db.getAccountProperty(accountID, "status");
+ return (status != "disabled");
+ },
+
+ isConnected: function (accountID) {
+ let status = TbSync.db.getAccountProperty(accountID, "status");
+ let validFolders = TbSync.db.findFolders({"cached": false}, {"accountID": accountID});
+ return (status != "disabled" && validFolders.length > 0);
+ },
+
+ resetSyncDataObj: function (accountID) {
+ this.syncDataObj[accountID] = new TbSync.SyncData(accountID);
+ },
+
+ getSyncDataObject: function (accountID) {
+ if (!this.syncDataObj.hasOwnProperty(accountID)) {
+ this.resetSyncDataObj(accountID);
+ }
+ return this.syncDataObj[accountID];
+ },
+
+ getNextPendingFolder: function (syncData) {
+ let sortedFolders = TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.getSortedFolders(syncData.accountData);
+ for (let i=0; i < sortedFolders.length; i++) {
+ if (sortedFolders[i].getFolderProperty("status") != "pending") continue;
+ syncData._setCurrentFolderData(sortedFolders[i]);
+ return true;
+ }
+ syncData._clearCurrentFolderData();
+ return false;
+ },
+
+
+ syncAllAccounts: function () {
+ //get info of all accounts
+ let accounts = TbSync.db.getAccounts();
+
+ for (let i=0; i < accounts.IDs.length; i++) {
+ // core async sync function, but we do not wait until it has finished,
+ // but return right away and initiate sync of all accounts parallel
+ this.syncAccount(accounts.IDs[i]);
+ }
+ },
+
+ syncAccount: async function (accountID, aSyncDescription = {}) {
+ let syncDescription = {};
+ Object.assign(syncDescription, aSyncDescription);
+
+ if (!syncDescription.hasOwnProperty("maxAccountReruns")) syncDescription.maxAccountReruns = 2;
+ if (!syncDescription.hasOwnProperty("maxFolderReruns")) syncDescription.maxFolderReruns = 2;
+ if (!syncDescription.hasOwnProperty("syncList")) syncDescription.syncList = true;
+ if (!syncDescription.hasOwnProperty("syncFolders")) syncDescription.syncFolders = null; // null ( = default = sync selected folders) or (empty) Array with folderData obj to be synced
+ if (!syncDescription.hasOwnProperty("syncJob")) syncDescription.syncJob = "sync";
+
+ //do not init sync if there is a sync running or account is not enabled
+ if (!this.isEnabled(accountID) || this.isSyncing(accountID)) return;
+
+ //create syncData object for each account (to be able to have parallel XHR)
+ this.resetSyncDataObj(accountID);
+ let syncData = this.getSyncDataObject(accountID);
+
+ //send GUI into lock mode (status == syncing)
+ TbSync.db.setAccountProperty(accountID, "status", "syncing");
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateAccountSettingsGui", accountID);
+
+ let overallStatusData = new TbSync.StatusData();
+ let accountRerun;
+ let accountRuns = 0;
+
+ do {
+ accountRerun = false;
+
+ if (accountRuns > syncDescription.maxAccountReruns) {
+ overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "resync-loop");
+ break;
+ }
+ accountRuns++;
+
+ if (syncDescription.syncList) {
+ let listStatusData;
+ try {
+ listStatusData = await TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.syncFolderList(syncData, syncDescription.syncJob, accountRuns);
+ } catch (e) {
+ listStatusData = new TbSync.StatusData(TbSync.StatusData.WARNING, "JavaScriptError", e.message + "\n\n" + e.stack);
+ }
+
+ if (!(listStatusData instanceof TbSync.StatusData)) {
+ overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "apiError", "TbSync/"+syncData.accountData.getAccountProperty("provider")+": Base.syncFolderList() must return a StatusData object");
+ break;
+ }
+
+ //if we have an error during folderList sync, there is no need to go on
+ if (listStatusData.type != TbSync.StatusData.SUCCESS) {
+ overallStatusData = listStatusData;
+ accountRerun = (listStatusData.type == TbSync.StatusData.ACCOUNT_RERUN)
+ TbSync.eventlog.add(listStatusData.type, syncData.eventLogInfo, listStatusData.message, listStatusData.details);
+ continue; //jumps to the while condition check
+ }
+
+ // Removes all leftover cached folders and sets all other folders to a well defined cached = "0"
+ // which will set this account as connected (if at least one non-cached folder is present).
+ this.removeCachedFolders(syncData);
+
+ // update folder list in GUI
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID);
+ }
+
+ // syncDescription.syncFolders is either null ( = default = sync selected folders) or an Array.
+ // Skip folder sync if Array is empty.
+ if (!Array.isArray(syncDescription.syncFolders) || syncDescription.syncFolders.length > 0) {
+ this.prepareFoldersForSync(syncData, syncDescription);
+
+ // update folder list in GUI
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID);
+
+ // if any folder was found, sync
+ if (syncData.accountData.isConnected()) {
+ let folderRuns = 1;
+ do {
+ if (folderRuns > syncDescription.maxFolderReruns) {
+ overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "resync-loop");
+ break;
+ }
+
+ // getNextPendingFolder will set or clear currentFolderData of syncData
+ if (!this.getNextPendingFolder(syncData)) {
+ break;
+ }
+
+ let folderStatusData;
+ try {
+ folderStatusData = await TbSync.providers[syncData.accountData.getAccountProperty("provider")].Base.syncFolder(syncData, syncDescription.syncJob, folderRuns);
+ } catch (e) {
+ folderStatusData = new TbSync.StatusData(TbSync.StatusData.WARNING, "JavaScriptError", e.message + "\n\n" + e.stack);
+ }
+
+ if (!(folderStatusData instanceof TbSync.StatusData)) {
+ folderStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "apiError", "TbSync/"+syncData.accountData.getAccountProperty("provider")+": Base.syncFolder() must return a StatusData object");
+ }
+
+ // if one of the folders indicated a FOLDER_RERUN, do not finish this
+ // folder but do it again
+ if (folderStatusData.type == TbSync.StatusData.FOLDER_RERUN) {
+ TbSync.eventlog.add(folderStatusData.type, syncData.eventLogInfo, folderStatusData.message, folderStatusData.details);
+ folderRuns++;
+ continue;
+ } else {
+ folderRuns = 1;
+ }
+
+ this.finishFolderSync(syncData, folderStatusData);
+
+ //if one of the folders indicated an ERROR, abort sync
+ if (folderStatusData.type == TbSync.StatusData.ERROR) {
+ break;
+ }
+
+ //if the folder has send an ACCOUNT_RERUN, abort sync and rerun the entire account
+ if (folderStatusData.type == TbSync.StatusData.ACCOUNT_RERUN) {
+ syncDescription.syncList = true;
+ accountRerun = true;
+ break;
+ }
+
+ } while (true);
+ } else {
+ overallStatusData = new TbSync.StatusData(TbSync.StatusData.ERROR, "no-folders-found-on-server");
+ }
+ }
+
+ } while (accountRerun);
+
+ this.finishAccountSync(syncData, overallStatusData);
+ },
+
+ // this could be added to AccountData, but I do not want that in public
+ setTargetModified: function (folderData) {
+ if (!folderData.accountData.isSyncing() && folderData.accountData.isEnabled()) {
+ folderData.accountData.setAccountProperty("status", "notsyncronized");
+ folderData.setFolderProperty("status", "modified");
+ //notify settings gui to update status
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ }
+ },
+
+ enableAccount: function(accountID) {
+ let accountData = new TbSync.AccountData(accountID);
+ TbSync.providers[accountData.getAccountProperty("provider")].Base.onEnableAccount(accountData);
+ accountData.setAccountProperty("status", "notsyncronized");
+ accountData.resetAccountProperty("lastsynctime");
+ },
+
+ disableAccount: function(accountID) {
+ let accountData = new TbSync.AccountData(accountID);
+ TbSync.providers[accountData.getAccountProperty("provider")].Base.onDisableAccount(accountData);
+ accountData.setAccountProperty("status", "disabled");
+
+ let folders = accountData.getAllFolders();
+ for (let folder of folders) {
+ if (folder.getFolderProperty("selected")) {
+ folder.targetData.removeTarget();
+ folder.setFolderProperty("selected", false);
+ }
+ folder.setFolderProperty("cached", true);
+ }
+ },
+
+ //removes all leftover cached folders and sets all other folders to a well defined cached = "0"
+ //which will set this account as connected (if at least one non-cached folder is present)
+ removeCachedFolders: function(syncData) {
+ let folders = syncData.accountData.getAllFoldersIncludingCache();
+ for (let folder of folders) {
+ //delete all leftover cached folders
+ if (folder.getFolderProperty("cached")) {
+ TbSync.db.deleteFolder(folder.accountID, folder.folderID);
+ continue;
+ } else {
+ //set well defined cache state
+ folder.setFolderProperty("cached", false);
+ }
+ }
+ },
+
+ //set allrequested folders to "pending", so they are marked for syncing
+ prepareFoldersForSync: function(syncData, syncDescription) {
+ let folders = syncData.accountData.getAllFolders();
+ for (let folder of folders) {
+ let requested = (Array.isArray(syncDescription.syncFolders) && syncDescription.syncFolders.filter(f => f.folderID == folder.folderID).length > 0);
+ let selected = (!Array.isArray(syncDescription.syncFolders) && folder.getFolderProperty("selected"));
+
+ //set folders to pending, so they get synced
+ if (requested || selected) {
+ folder.setFolderProperty("status", "pending");
+ }
+ }
+ },
+
+ finishFolderSync: function(syncData, statusData) {
+ if (statusData.type != TbSync.StatusData.SUCCESS) {
+ //report error
+ TbSync.eventlog.add(statusData.type, syncData.eventLogInfo, statusData.message, statusData.details);
+ }
+
+ //if this is a success, prepend success to the status message,
+ //otherwise just set the message
+ let status;
+ if (statusData.type == TbSync.StatusData.SUCCESS || statusData.message == "") {
+ status = statusData.type;
+ if (statusData.message) status = status + "." + statusData.message;
+ } else {
+ status = statusData.message;
+ }
+
+ if (syncData.currentFolderData) {
+ syncData.currentFolderData.setFolderProperty("status", status);
+ syncData.currentFolderData.setFolderProperty("lastsynctime", Date.now());
+ //clear folderID to fall back to account-only-mode (folder is done!)
+ syncData._clearCurrentFolderData();
+ }
+
+ syncData.setSyncState("done");
+ },
+
+ finishAccountSync: function(syncData, statusData) {
+ // set each folder with PENDING status to ABORTED
+ let folders = TbSync.db.findFolders({"status": "pending"}, {"accountID": syncData.accountData.accountID});
+ for (let i=0; i < folders.length; i++) {
+ TbSync.db.setFolderProperty(folders[i].accountID, folders[i].folderID, "status", "aborted");
+ }
+
+ //if this is a success, prepend success to the status message,
+ //otherwise just set the message
+ let status;
+ if (statusData.type == TbSync.StatusData.SUCCESS || statusData.message == "") {
+ status = statusData.type;
+ if (statusData.message) status = status + "." + statusData.message;
+ } else {
+ status = statusData.message;
+ }
+
+
+ if (statusData.type != TbSync.StatusData.SUCCESS) {
+ //report error
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, statusData.message, statusData.details);
+ } else {
+ //account itself is ok, search for folders with error
+ folders = TbSync.db.findFolders({"selected": true, "cached": false}, {"accountID": syncData.accountData.accountID});
+ for (let i in folders) {
+ let folderstatus = folders[i].data.status.split(".")[0];
+ if (folderstatus != "" && folderstatus != TbSync.StatusData.SUCCESS && folderstatus != "aborted") {
+ status = "foldererror";
+ break;
+ }
+ }
+ }
+
+ //done
+ syncData.accountData.setAccountProperty("lastsynctime", Date.now());
+ syncData.accountData.setAccountProperty("status", status);
+ syncData.setSyncState("accountdone");
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateFolderList", syncData.accountData.accountID);
+ this.resetSyncDataObj(syncData.accountData.accountID);
+ }
+
+}
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;
+ }
+};
diff --git a/content/modules/eventlog.js b/content/modules/eventlog.js
new file mode 100644
index 0000000..ef8f1e8
--- /dev/null
+++ b/content/modules/eventlog.js
@@ -0,0 +1,153 @@
+/*
+ * 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 EventLogInfo = class {
+ /**
+ * An EventLogInfo instance is used when adding entries to the
+ * :ref:`TbSyncEventLog`. The information given here will be added as a
+ * header to the actual event.
+ *
+ * @param {string} provider ``Optional`` A provider ID (also used as
+ * provider namespace).
+ * @param {string} accountname ``Optional`` An account name. Can be
+ * arbitrary but should match the accountID
+ * (if provided).
+ * @param {string} accountID ``Optional`` An account ID. Used to filter
+ * events for a given account.
+ * @param {string} foldername ``Optional`` A folder name.
+ *
+ */
+ constructor(provider, accountname = "", accountID = "", foldername = "") {
+ this._provider = provider;
+ this._accountname = accountname;
+ this._accountID = accountID;
+ this._foldername = foldername;
+ }
+
+ /**
+ * Getter/Setter for the provider ID of this EventLogInfo.
+ */
+ get provider() {return this._provider};
+ /**
+ * Getter/Setter for the account ID of this EventLogInfo.
+ */
+ get accountname() {return this._accountname};
+ /**
+ * Getter/Setter for the account name of this EventLogInfo.
+ */
+ get accountID() {return this._accountID};
+ /**
+ * Getter/Setter for the folder name of this EventLogInfo.
+ */
+ get foldername() {return this._foldername};
+
+ set provider(v) {this._provider = v};
+ set accountname(v) {this._accountname = v};
+ set accountID(v) {this._accountID = v};
+ set foldername(v) {this._foldername = v};
+}
+
+
+
+/**
+ * The TbSync event log
+ */
+var eventlog = {
+ /**
+ * Adds an entry to the TbSync event log
+ *
+ * @param {StatusDataType} type One of the types defined in
+ * :class:`StatusData`
+ * @param {EventLogInfo} eventInfo EventLogInfo for this event.
+ * @param {string} message The event message.
+ * @param {string} details ``Optional`` The event details.
+ *
+ */
+ add: function (type, eventInfo, message, details = null) {
+ let entry = {
+ timestamp: Date.now(),
+ message: message,
+ type: type,
+ link: null,
+ //some details are just true, which is not a useful detail, ignore
+ details: details === true ? null : details,
+ provider: "",
+ accountname: "",
+ foldername: "",
+ };
+
+ if (eventInfo) {
+ if (eventInfo.accountID) entry.accountID = eventInfo.accountID;
+ if (eventInfo.provider) entry.provider = eventInfo.provider;
+ if (eventInfo.accountname) entry.accountname = eventInfo.accountname;
+ if (eventInfo.foldername) entry.foldername = eventInfo.foldername;
+ }
+
+ let localized = "";
+ let link = "";
+ if (entry.provider) {
+ localized = TbSync.getString("status." + message, entry.provider);
+ link = TbSync.getString("helplink." + message, entry.provider);
+ } else {
+ //try to get localized string from message from TbSync
+ localized = TbSync.getString("status." + message);
+ link = TbSync.getString("helplink." + message);
+ }
+
+ //can we provide a localized version of the event msg?
+ if (localized != "status."+message) {
+ entry.message = localized;
+ }
+
+ //is there a help link?
+ if (link != "helplink." + message) {
+ entry.link = link;
+ }
+
+ //dump the non-localized message into debug log
+ TbSync.dump("EventLog", message + (entry.details !== null ? "\n" + entry.details : ""));
+ this.events.push(entry);
+ if (this.events.length > 100) this.events.shift();
+ Services.obs.notifyObservers(null, "tbsync.observer.eventlog.update", null);
+ },
+
+ events: null,
+ eventLogWindow: null,
+
+ load: async function () {
+ this.clear();
+ },
+
+ unload: async function () {
+ if (this.eventLogWindow) {
+ this.eventLogWindow.close();
+ }
+ },
+
+ get: function (accountID = null) {
+ if (accountID) {
+ return this.events.filter(e => e.accountID == accountID);
+ } else {
+ return this.events;
+ }
+ },
+
+ clear: function () {
+ this.events = [];
+ },
+
+
+ open: function (accountID = null, folderID = null) {
+ this.eventLogWindow = TbSync.manager.prefWindowObj.open("chrome://tbsync/content/manager/eventlog/eventlog.xhtml", "TbSyncEventLog", "centerscreen,chrome,resizable");
+ },
+}
diff --git a/content/modules/io.js b/content/modules/io.js
new file mode 100644
index 0000000..a4ecbdb
--- /dev/null
+++ b/content/modules/io.js
@@ -0,0 +1,41 @@
+/*
+ * 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 io = {
+
+ storageDirectory : PathUtils.join(PathUtils.profileDir, "TbSync"),
+
+ load: async function () {
+ },
+
+ unload: async function () {
+ },
+
+ getAbsolutePath: function(filename) {
+ return PathUtils.join(this.storageDirectory, filename);
+ },
+
+ initFile: function (filename) {
+ let file = FileUtils.getFile("ProfD", ["TbSync",filename]);
+ //create a stream to write to that file
+ let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
+ foStream.init(file, 0x02 | 0x08 | 0x20, parseInt("0666", 8), 0); // write, create, truncate
+ foStream.close();
+ },
+
+ appendToFile: function (filename, data) {
+ let file = FileUtils.getFile("ProfD", ["TbSync",filename]);
+ //create a strem to write to that file
+ let foStream = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
+ foStream.init(file, 0x02 | 0x08 | 0x10, parseInt("0666", 8), 0); // write, create, append
+ foStream.write(data, data.length);
+ foStream.close();
+ },
+}
diff --git a/content/modules/lightning.js b/content/modules/lightning.js
new file mode 100644
index 0000000..cd9a383
--- /dev/null
+++ b/content/modules/lightning.js
@@ -0,0 +1,774 @@
+/*
+ * 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 { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+ XPCOMUtils.defineLazyModuleGetters(this, {
+ CalAlarm: "resource:///modules/CalAlarm.jsm",
+ CalAttachment: "resource:///modules/CalAttachment.jsm",
+ CalAttendee: "resource:///modules/CalAttendee.jsm",
+ CalEvent: "resource:///modules/CalEvent.jsm",
+ CalTodo: "resource:///modules/CalTodo.jsm",
+});
+
+var lightning = {
+
+ cal: null,
+ ICAL: null,
+
+ load: async function () {
+ try {
+ TbSync.lightning.cal = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm").cal;
+ TbSync.lightning.ICAL = ChromeUtils.import("resource:///modules/calendar/Ical.jsm").ICAL;
+ let manager = TbSync.lightning.cal.manager;
+ manager.addCalendarObserver(this.calendarObserver);
+ manager.addObserver(this.calendarManagerObserver);
+ } catch (e) {
+ TbSync.dump("Check4Lightning","Error during lightning module import: " + e.toString() + "\n" + e.stack);
+ Components.utils.reportError(e);
+ }
+ },
+
+ unload: async function () {
+ //removing global observer
+ let manager = TbSync.lightning.cal.manager;
+ manager.removeCalendarObserver(this.calendarObserver);
+ manager.removeObserver(this.calendarManagerObserver);
+
+ //remove listeners on global sync buttons
+ if (TbSync.window.document.getElementById("calendar-synchronize-button")) {
+ TbSync.window.document.getElementById("calendar-synchronize-button").removeEventListener("click", function(event){Services.obs.notifyObservers(null, 'tbsync.observer.sync', null);}, false);
+ }
+ if (TbSync.window.document.getElementById("task-synchronize-button")) {
+ TbSync.window.document.getElementById("task-synchronize-button").removeEventListener("click", function(event){Services.obs.notifyObservers(null, 'tbsync.observer.sync', null);}, false);
+ }
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * AdvancedTargetData, an extended TargetData implementation, providers
+ // * can use this as their own TargetData by extending it and just
+ // * defining the extra methods
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ AdvancedTargetData : class {
+ constructor(folderData) {
+ this._folderData = folderData;
+ this._targetObj = null;
+ }
+
+ // Check, if the target exists and return true/false.
+ hasTarget() {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ return calendar ? true : false;
+ }
+
+ // Returns the target obj, which TbSync should return as the target. It can
+ // be whatever you want and is returned by FolderData.targetData.getTarget().
+ // If the target does not exist, it should be created. Throw a simple Error, if that
+ // failed.
+ async getTarget() {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ if (!calendar) {
+ calendar = await TbSync.lightning.prepareAndCreateCalendar(this._folderData);
+ if (!calendar)
+ throw new Error("notargets");
+ }
+
+ if (!this._targetObj || this._targetObj.id != calendar.id)
+ this._targetObj = new TbSync.lightning.TbCalendar(calendar, this._folderData);
+
+ return this._targetObj;
+ }
+
+ /**
+ * Removes the target from the local storage. If it does not exist, return
+ * silently. A call to ``hasTarget()`` should return false, after this has
+ * been executed.
+ *
+ */
+ removeTarget() {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ try {
+ if (calendar) {
+ calManager.removeCalendar(calendar);
+ }
+ } catch (e) {}
+ TbSync.db.clearChangeLog(target);
+ this._folderData.resetFolderProperty("target");
+ }
+
+
+ /**
+ * Disconnects the target in the local storage from this TargetData, but
+ * does not delete it, so it becomes a stale "left over" . A call
+ * to ``hasTarget()`` should return false, after this has been executed.
+ *
+ */
+ disconnectTarget() {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ if (calendar) {
+ let changes = TbSync.db.getItemsFromChangeLog(target, 0, "_by_user");
+ if (changes.length > 0) {
+ this.targetName = this.targetName + " (*)";
+ }
+ calendar.setProperty("disabled", true);
+ calendar.setProperty("tbSyncProvider", "orphaned");
+ calendar.setProperty("tbSyncAccountID", "");
+ }
+ TbSync.db.clearChangeLog(target);
+ this._folderData.resetFolderProperty("target");
+ }
+
+ set targetName(newName) {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ if (calendar) {
+ calendar.name = newName;
+ } else {
+ throw new Error("notargets");
+ }
+ }
+
+ get targetName() {
+ let calManager = TbSync.lightning.cal.manager;
+ let target = this._folderData.getFolderProperty("target");
+ let calendar = calManager.getCalendarById(target);
+
+ if (calendar) {
+ return calendar.name;
+ } else {
+ throw new Error("notargets");
+ }
+ }
+
+ setReadOnly(value) {
+ // hasTarget() can throw an error, ignore that here
+ try {
+ if (this.hasTarget()) {
+ this.getTarget().then(target => target.calendar.setProperty("readOnly", value));
+ }
+ } catch (e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+
+ // * * * * * * * * * * * * * * * * *
+ // * AdvancedTargetData extension *
+ // * * * * * * * * * * * * * * * * *
+
+ get isAdvancedCalendarTargetData() {
+ return true;
+ }
+
+ get folderData() {
+ return this._folderData;
+ }
+
+ // The calendar target does not support a custom primaryKeyField, because
+ // the lightning implementation only allows to search for items via UID.
+ // Like the addressbook target, the calendar target item element has a
+ // primaryKey getter/setter which - however - only works on the UID.
+
+ // enable or disable changelog
+ get logUserChanges(){
+ return true;
+ }
+
+ calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) {
+ switch (aTopic) {
+ case "onCalendarPropertyChanged":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName);
+ break;
+
+ case "onCalendarDeleted":
+ case "onCalendarPropertyDeleted":
+ //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name);
+ break;
+ }
+ }
+
+ itemObserver(aTopic, tbItem, tbOldItem) {
+ switch (aTopic) {
+ case "onAddItem":
+ case "onModifyItem":
+ case "onDeleteItem":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title);
+ break;
+ }
+ }
+
+ // replace this with your own implementation to create the actual addressbook,
+ // when this class is extended
+ async createCalendar(newname) {
+ let calManager = TbSync.lightning.cal.manager;
+ let newCalendar = calManager.createCalendar("storage", Services.io.newURI("moz-storage-calendar://"));
+ newCalendar.id = TbSync.lightning.cal.getUUID();
+ newCalendar.name = newname;
+ return newCalendar
+ }
+
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * TbItem and TbCalendar Classes
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ TbItem : class {
+ constructor(TbCalendar, item) {
+ if (!TbCalendar)
+ throw new Error("TbItem::constructor is missing its first parameter!");
+
+ if (!item)
+ throw new Error("TbItem::constructor is missing its second parameter!");
+
+ this._tbCalendar = TbCalendar;
+ this._item = item;
+
+ this._isTodo = (item instanceof Ci.calITodo);
+ this._isEvent = (item instanceof Ci.calIEvent);
+ }
+
+ get tbCalendar() {
+ return this._tbCalendar;
+ }
+
+ get isTodo() {
+ return this._isTodo;
+ }
+
+ get isEvent() {
+ return this._isEvent;
+ }
+
+
+
+
+
+ get nativeItem() {
+ return this._item;
+ }
+
+ get UID() {
+ return this._item.id;
+ }
+
+ get primaryKey() {
+ // no custom key possible with lightning, must use the UID
+ return this._item.id;
+ }
+
+ set primaryKey(value) {
+ // no custom key possible with lightning, must use the UID
+ this._item.id = value;
+ }
+
+ clone() {
+ return new TbSync.lightning.TbItem(this._tbCalendar, this._item.clone());
+ }
+
+ toString() {
+ return this._item.icalString;
+ }
+
+ getProperty(property, fallback = "") {
+ return this._item.hasProperty(property) ? this._item.getProperty(property) : fallback;
+ }
+
+ setProperty(property, value) {
+ this._item.setProperty(property, value);
+ }
+
+ deleteProperty(property) {
+ this._item.deleteProperty(property);
+ }
+
+ get changelogData() {
+ return TbSync.db.getItemDataFromChangeLog(this._tbCalendar.UID, this.primaryKey);
+ }
+
+ get changelogStatus() {
+ return TbSync.db.getItemStatusFromChangeLog(this._tbCalendar.UID, this.primaryKey);
+ }
+
+ set changelogStatus(status) {
+ let value = this.primaryKey;
+
+ if (value) {
+ if (!status) {
+ TbSync.db.removeItemFromChangeLog(this._tbCalendar.UID, value);
+ return;
+ }
+
+ if (this._tbCalendar.logUserChanges || status.endsWith("_by_server")) {
+ TbSync.db.addItemToChangeLog(this._tbCalendar.UID, value, status);
+ }
+ }
+ }
+ },
+
+
+ TbCalendar : class {
+ constructor(calendar, folderData) {
+ this._calendar = calendar;
+ this._folderData = folderData;
+ }
+
+ get calendar() {
+ return this._calendar;
+ }
+
+ get promisifyCalendar() {
+ return this._calendar;
+ }
+
+ get logUserChanges() {
+ return this._folderData.targetData.logUserChanges;
+ }
+
+ get primaryKeyField() {
+ // Not supported by lightning. We let the implementation sit here, it may get changed in the future.
+ // In order to support this, lightning needs to implement a proper getItemfromProperty() method.
+ return null;
+ }
+
+ get UID() {
+ return this._calendar.id;
+ }
+
+ createNewEvent() {
+ let event = new CalEvent();
+ return new TbSync.lightning.TbItem(this, event);
+ }
+
+ createNewTodo() {
+ let todo = new CalTodo();
+ return new TbSync.lightning.TbItem(this, todo);
+ }
+
+
+
+
+ async addItem(tbItem, pretagChangelogWithByServerEntry = true) {
+ if (this.primaryKeyField && !tbItem.getProperty(this.primaryKeyField)) {
+ tbItem.setProperty(this.primaryKeyField, this._folderData.targetData.generatePrimaryKey());
+ //Services.console.logStringMessage("[TbCalendar::addItem] Generated primary key!");
+ }
+
+ if (pretagChangelogWithByServerEntry) {
+ tbItem.changelogStatus = "added_by_server";
+ }
+ return await this._calendar.addItem(tbItem._item);
+ }
+
+ async modifyItem(tbNewItem, tbOldItem, pretagChangelogWithByServerEntry = true) {
+ // only add entry if the current entry does not start with _by_user
+ let status = tbNewItem.changelogStatus ? tbNewItem.changelogStatus : "";
+ if (pretagChangelogWithByServerEntry && !status.endsWith("_by_user")) {
+ tbNewItem.changelogStatus = "modified_by_server";
+ }
+
+ return await this._calendar.modifyItem(tbNewItem._item, tbOldItem._item);
+ }
+
+ async deleteItem(tbItem, pretagChangelogWithByServerEntry = true) {
+ if (pretagChangelogWithByServerEntry) {
+ tbItem.changelogStatus = "deleted_by_server";
+ }
+ return await this._calendar.deleteItem(tbItem._item);
+ }
+
+ // searchId is interpreted as the primaryKeyField, which is the UID for this target
+ async getItem (searchId) {
+ let item = await this._calendar.getItem(searchId);
+ if (item) {
+ return new TbSync.lightning.TbItem(this, item);
+ }
+ return null;
+ }
+
+ async getItemFromProperty(property, value) {
+ if (property == "UID") return await this.getItem(value);
+ else throw ("TbSync.lightning.getItemFromProperty: Currently onle the UID property can be used to search for items.");
+ }
+
+ async getAllItems() {
+ return await this._calendar.getItems(Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, 0, null, null);
+ }
+
+ getAddedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "added_by_user").map(item => item.itemId);
+ }
+
+ getModifiedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "modified_by_user").map(item => item.itemId);
+ }
+
+ getDeletedItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "deleted_by_user").map(item => item.itemId);
+ }
+
+ getItemsFromChangeLog(maxitems = 0) {
+ return TbSync.db.getItemsFromChangeLog(this.calendar.id, maxitems, "_by_user");
+ }
+
+ removeItemFromChangeLog(id, moveToEndInsteadOfDelete = false) {
+ TbSync.db.removeItemFromChangeLog(this.calendar.id, id, moveToEndInsteadOfDelete);
+ }
+
+ clearChangelog() {
+ TbSync.db.clearChangeLog(this.calendar.id);
+ }
+ },
+
+
+
+
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // * Internal Functions
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+ getFolderFromCalendarUID: function(calUID) {
+ let folders = TbSync.db.findFolders({"target": calUID});
+ if (folders.length == 1) {
+ let accountData = new TbSync.AccountData(folders[0].accountID);
+ return new TbSync.FolderData(accountData, folders[0].folderID);
+ }
+ return null;
+ },
+
+ getFolderFromCalendarURL: function(calURL) {
+ let folders = TbSync.db.findFolders({"url": calURL});
+ if (folders.length == 1) {
+ let accountData = new TbSync.AccountData(folders[0].accountID);
+ return new TbSync.FolderData(accountData, folders[0].folderID);
+ }
+ return null;
+ },
+
+ calendarObserver : {
+ onStartBatch : function () {},
+ onEndBatch : function () {},
+ onLoad : function (aCalendar) {},
+ onError : function (aCalendar, aErrNo, aMessage) {},
+
+ onAddItem : function (aAddedItem) {
+ if (!(aAddedItem && aAddedItem.calendar))
+ return;
+
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aAddedItem.calendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aAddedItem.calendar, folderData);
+ let tbItem = new TbSync.lightning.TbItem(tbCalendar, aAddedItem);
+ let itemStatus = tbItem.changelogStatus;
+
+ // if this card was created by us, it will be in the log
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ let age = Date.now() - tbItem.changelogData.timestamp;
+ if (age < 1500) {
+ // during freeze, local modifications are not possible
+ return;
+ } else {
+ // remove blocking entry from changelog after freeze time is over (1.5s),
+ // and continue evaluating this event
+ abItem.changelogStatus = "";
+ }
+ }
+
+ if (itemStatus == "deleted_by_user") {
+ // deleted ? user moved item out and back in -> modified
+ tbItem.changelogStatus = "modified_by_user";
+ } else {
+ tbItem.changelogStatus = "added_by_user";
+ }
+
+ if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.itemObserver("onAddItem", tbItem, null);
+ }
+ },
+
+ onModifyItem : function (aNewItem, aOldItem) {
+ //check, if it is a pure modification within the same calendar
+ if (!(aNewItem && aNewItem.calendar && aOldItem && aOldItem.calendar && aNewItem.calendar.id == aOldItem.calendar.id))
+ return;
+
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aNewItem.calendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aNewItem.calendar, folderData);
+ let tbNewItem = new TbSync.lightning.TbItem(tbCalendar, aNewItem);
+ let tbOldItem = new TbSync.lightning.TbItem(tbCalendar, aOldItem);
+ let itemStatus = tbNewItem.changelogStatus;
+
+ // if this card was created by us, it will be in the log
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ let age = Date.now() - tbNewItem.changelogData.timestamp;
+ if (age < 1500) {
+ // during freeze, local modifications are not possible
+ return;
+ } else {
+ // remove blocking entry from changelog after freeze time is over (1.5s),
+ // and continue evaluating this event
+ tbNewItem.changelogStatus = "";
+ }
+ }
+
+ if (itemStatus != "added_by_user") {
+ //added_by_user -> it is a local unprocessed add do not re-add it to changelog
+ tbNewItem.changelogStatus = "modified_by_user";
+ }
+
+ if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.itemObserver("onModifyItem", tbNewItem, tbOldItem);
+ }
+ },
+
+ onDeleteItem : function (aDeletedItem) {
+ if (!(aDeletedItem && aDeletedItem.calendar))
+ return;
+
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aDeletedItem.calendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aDeletedItem.calendar, folderData);
+ let tbItem = new TbSync.lightning.TbItem(tbCalendar, aDeletedItem);
+ let itemStatus = tbItem.changelogStatus;
+
+ // if this card was created by us, it will be in the log
+ if (itemStatus && itemStatus.endsWith("_by_server")) {
+ let age = Date.now() - tbItem.changelogData.timestamp;
+ if (age < 1500) {
+ // during freeze, local modifications are not possible
+ return;
+ } else {
+ // remove blocking entry from changelog after freeze time is over (1.5s),
+ // and continue evaluating this event
+ tbItem.changelogStatus = "";
+ }
+ }
+
+ if (itemStatus == "added_by_user") {
+ //a local add, which has not yet been processed (synced) is deleted -> remove all traces
+ tbItem.changelogStatus = "";
+ } else {
+ tbItem.changelogStatus = "deleted_by_user";
+ }
+
+ if (tbCalendar.logUserChanges) TbSync.core.setTargetModified(folderData);
+ folderData.targetData.itemObserver("onDeleteItem", tbItem, null);
+ }
+ },
+
+ //Changed properties of the calendar itself (name, color etc.)
+ onPropertyChanged : function (aCalendar, aName, aValue, aOldValue) {
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData);
+
+ switch (aName) {
+ case "color":
+ // update stored color to recover after disable
+ folderData.setFolderProperty("targetColor", aValue);
+ break;
+ case "name":
+ // update stored name to recover after disable
+ folderData.setFolderProperty("targetName", aValue);
+ // update settings window, if open
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ break;
+ }
+
+ folderData.targetData.calendarObserver("onCalendarPropertyChanged", tbCalendar, aName, aValue, aOldValue);
+ }
+ },
+
+ //Deleted properties of the calendar itself (name, color etc.)
+ onPropertyDeleting : function (aCalendar, aName) {
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData);
+
+ switch (aName) {
+ case "color":
+ case "name":
+ //update settings window, if open
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ break;
+ }
+
+ folderData.targetData.calendarObserver("onCalendarPropertyDeleted", tbCalendar, aName);
+ }
+ }
+ },
+
+ calendarManagerObserver : {
+ onCalendarRegistered : function (aCalendar) {
+ },
+
+ onCalendarUnregistering : function (aCalendar) {
+ /*let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ folderData.targetData.calendarObserver("onCalendarUnregistered", aCalendar);
+ }*/
+ },
+
+ onCalendarDeleting : async function (aCalendar) {
+ let folderData = TbSync.lightning.getFolderFromCalendarUID(aCalendar.id);
+ if (folderData
+ && folderData.targetData
+ && folderData.targetData.isAdvancedCalendarTargetData) {
+
+ // If the user switches "offline support", the calendar is deleted and recreated. Thus,
+ // we wait a bit and check, if the calendar is back again and ignore the delete event.
+ if (aCalendar.type == "caldav") {
+ await TbSync.tools.sleep(1500);
+ let calManager = TbSync.lightning.cal.manager;
+ for (let calendar of calManager.getCalendars({})) {
+ if (calendar.uri.spec == aCalendar.uri.spec) {
+ // update the target
+ folderData.setFolderProperty("target", calendar.id)
+ return;
+ }
+ }
+ }
+
+ //delete any pending changelog of the deleted calendar
+ TbSync.db.clearChangeLog(aCalendar.id);
+
+ let tbCalendar = new TbSync.lightning.TbCalendar(aCalendar, folderData);
+
+ //unselect calendar if deleted by user and update settings window, if open
+ if (folderData.getFolderProperty("selected")) {
+ folderData.setFolderProperty("selected", false);
+ //update settings window, if open
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folderData.accountID);
+ }
+
+ folderData.resetFolderProperty("target");
+ folderData.targetData.calendarObserver("onCalendarDeleted", tbCalendar);
+
+ }
+ },
+ },
+
+
+
+ //this function actually creates a calendar if missing
+ prepareAndCreateCalendar: async function (folderData) {
+ let calManager = TbSync.lightning.cal.manager;
+ let provider = folderData.accountData.getAccountProperty("provider");
+
+ //check if there is a known/cached name, and use that as starting point to generate unique name for new calendar
+ let cachedName = folderData.getFolderProperty("targetName");
+ let newname = cachedName == "" ? folderData.accountData.getAccountProperty("accountname") + " (" + folderData.getFolderProperty("foldername") + ")" : cachedName;
+
+ //check if there is a cached or preloaded color - if not, chose one
+ if (!folderData.getFolderProperty("targetColor")) {
+ //define color set
+ let allColors = [
+ "#3366CC",
+ "#DC3912",
+ "#FF9900",
+ "#109618",
+ "#990099",
+ "#3B3EAC",
+ "#0099C6",
+ "#DD4477",
+ "#66AA00",
+ "#B82E2E",
+ "#316395",
+ "#994499",
+ "#22AA99",
+ "#AAAA11",
+ "#6633CC",
+ "#E67300",
+ "#8B0707",
+ "#329262",
+ "#5574A6",
+ "#3B3EAC"];
+
+ //find all used colors
+ let usedColors = [];
+ for (let calendar of calManager.getCalendars({})) {
+ if (calendar && calendar.getProperty("color")) {
+ usedColors.push(calendar.getProperty("color").toUpperCase());
+ }
+ }
+
+ //we do not want to change order of colors, we want to FILTER by counts, so we need to find the least count, filter by that and then take the first one
+ let minCount = null;
+ let statColors = [];
+ for (let i=0; i< allColors.length; i++) {
+ let count = usedColors.filter(item => item == allColors[i]).length;
+ if (minCount === null) minCount = count;
+ else if (count < minCount) minCount = count;
+
+ let obj = {};
+ obj.color = allColors[i];
+ obj.count = count;
+ statColors.push(obj);
+ }
+
+ //filter by minCount
+ let freeColors = statColors.filter(item => (minCount == null || item.count == minCount));
+ folderData.setFolderProperty("targetColor", freeColors[0].color);
+ }
+
+ //create and register new calendar
+ let newCalendar = await folderData.targetData.createCalendar(newname);
+ newCalendar.setProperty("tbSyncProvider", provider);
+ newCalendar.setProperty("tbSyncAccountID", folderData.accountData.accountID);
+
+ //store id of calendar as target in DB
+ folderData.setFolderProperty("target", newCalendar.id);
+ folderData.setFolderProperty("targetName", newCalendar.name);
+ folderData.setFolderProperty("targetColor", newCalendar.getProperty("color"));
+ return newCalendar;
+ }
+}
diff --git a/content/modules/manager.js b/content/modules/manager.js
new file mode 100644
index 0000000..12ef4b2
--- /dev/null
+++ b/content/modules/manager.js
@@ -0,0 +1,392 @@
+/*
+ * 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 manager = {
+
+ prefWindowObj: null,
+
+ load: async function () {
+ },
+
+ unload: async function () {
+ //close window (if open)
+ if (this.prefWindowObj !== null) this.prefWindowObj.close();
+ },
+
+
+
+
+
+ openManagerWindow: function(event) {
+ if (!event.button) { //catches zero or undefined
+ if (TbSync.enabled) {
+ // check, if a window is already open and just put it in focus
+ if (this.prefWindowObj === null) {
+ this.prefWindowObj = TbSync.window.open("chrome://tbsync/content/manager/accountManager.xhtml", "TbSyncAccountManagerWindow", "chrome,centerscreen");
+ }
+ this.prefWindowObj.focus();
+ } else {
+ //this.popupNotEnabled();
+ }
+ }
+ },
+
+ popupNotEnabled: function () {
+ TbSync.dump("Oops", "Trying to open account manager, but init sequence not yet finished");
+ let msg = TbSync.getString("OopsMessage") + "\n\n";
+ let v = Services.appinfo.platformVersion;
+ if (TbSync.prefs.getIntPref("log.userdatalevel") == 0) {
+ if (TbSync.window.confirm(msg + TbSync.getString("UnableToTraceError"))) {
+ TbSync.prefs.setIntPref("log.userdatalevel", 1);
+ TbSync.window.alert(TbSync.getString("RestartThunderbirdAndTryAgain"));
+ }
+ } else {
+ if (TbSync.window.confirm(msg + TbSync.getString("HelpFixStartupError"))) {
+ this.createBugReport("john.bieling@gmx.de", msg, "");
+ }
+ }
+ },
+
+ openTBtab: function (url) {
+ let tabmail = TbSync.window.document.getElementById("tabmail");
+ if (TbSync.window && tabmail) {
+ TbSync.window.focus();
+ return tabmail.openTab("contentTab", {
+ url
+ });
+ }
+ return null;
+ },
+
+ openTranslatedLink: function (url) {
+ let googleCode = TbSync.getString("google.translate.code");
+ if (googleCode != "en" && googleCode != "google.translate.code") {
+ this.openLink("https://translate.google.com/translate?hl=en&sl=en&tl="+TbSync.getString("google.translate.code")+"&u="+url);
+ } else {
+ this.openLink(url);
+ }
+ },
+
+ openLink: function (url) {
+ let ioservice = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+ let uriToOpen = ioservice.newURI(url, null, null);
+ let extps = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
+ extps.loadURI(uriToOpen, null);
+ },
+
+ openBugReportWizard: function () {
+ if (!TbSync.debugMode) {
+ this.prefWindowObj.alert(TbSync.getString("NoDebugLog"));
+ } else {
+ this.prefWindowObj.openDialog("chrome://tbsync/content/manager/support-wizard/support-wizard.xhtml", "support-wizard", "dialog,centerscreen,chrome,resizable=no");
+ }
+ },
+
+ createBugReport: function (email, subject, description) {
+ let fields = Components.classes["@mozilla.org/messengercompose/composefields;1"].createInstance(Components.interfaces.nsIMsgCompFields);
+ let params = Components.classes["@mozilla.org/messengercompose/composeparams;1"].createInstance(Components.interfaces.nsIMsgComposeParams);
+
+ fields.to = email;
+ fields.subject = "TbSync " + TbSync.addon.version.toString() + " bug report: " + subject;
+ fields.body = "Hi,\n\n" +
+ "attached you find my debug.log for the following error:\n\n" +
+ description;
+
+ params.composeFields = fields;
+ params.format = Components.interfaces.nsIMsgCompFormat.PlainText;
+
+ let attachment = Components.classes["@mozilla.org/messengercompose/attachment;1"].createInstance(Components.interfaces.nsIMsgAttachment);
+ attachment.contentType = "text/plain";
+ attachment.url = 'file://' + TbSync.io.getAbsolutePath("debug.log");
+ attachment.name = "debug.log";
+ attachment.temporary = false;
+
+ params.composeFields.addAttachment(attachment);
+ MailServices.compose.OpenComposeWindowWithParams (null, params);
+ },
+
+ viewDebugLog: function() {
+
+ if (this.debugLogWindow && this.debugLogWindow.tabNode) {
+ let tabmail = TbSync.window.document.getElementById("tabmail");
+ try {
+ tabmail.closeTab(this.debugLogWindow);
+ } catch (e) {
+ // nope
+ }
+ this.debugLogWindow = null;
+ }
+ this.debugLogWindow = this.openTBtab('file://' + TbSync.io.getAbsolutePath("debug.log"));
+ },
+}
+
+
+
+/**
+ * Functions used by the folderlist in the main account settings tab
+ */
+manager.FolderList = class {
+ /**
+ * @param {string} provider Identifier for the provider this FolderListView is created for.
+ */
+ constructor(provider) {
+ this.provider = provider
+ }
+
+ /**
+ * Is called before the context menu of the folderlist is shown, allows to
+ * show/hide custom menu options based on selected folder
+ *
+ * @param document [in] document object of the account settings window - element.ownerDocument - menuentry?
+ * @param folderData [in] FolderData of the selected folder
+ */
+ onContextMenuShowing(window, folderData) {
+ return TbSync.providers[this.provider].StandardFolderList.onContextMenuShowing(window, folderData);
+ }
+
+
+ /**
+ * Returns an array of attribute objects, which define the number of columns
+ * and the look of the header
+ */
+ getHeader() {
+ return [
+ {style: "font-weight:bold;", label: "", width: "93"},
+ {style: "font-weight:bold;", label: TbSync.getString("manager.resource"), width:"150"},
+ {style: "font-weight:bold;", label: TbSync.getString("manager.status"), flex :"1"},
+ ]
+ }
+
+
+ /**
+ * Is called to add a row to the folderlist. After this call, updateRow is called as well.
+ *
+ * @param document [in] document object of the account settings window
+ * @param folderData [in] FolderData of the folder in the row
+ */
+ getRow(document, folderData) {
+ //create checkBox for select state
+ let itemSelCheckbox = document.createXULElement("checkbox");
+ itemSelCheckbox.setAttribute("updatefield", "selectbox");
+ itemSelCheckbox.setAttribute("style", "margin: 0px 0px 0px 3px;");
+ itemSelCheckbox.addEventListener("command", this.toggleFolder);
+
+ //icon
+ let itemType = document.createXULElement("image");
+ itemType.setAttribute("src", TbSync.providers[this.provider].StandardFolderList.getTypeImage(folderData));
+ itemType.setAttribute("style", "margin: 0px 9px 0px 3px;");
+
+ //ACL
+ let roAttributes = TbSync.providers[this.provider].StandardFolderList.getAttributesRoAcl(folderData);
+ let rwAttributes = TbSync.providers[this.provider].StandardFolderList.getAttributesRwAcl(folderData);
+ let itemACL = document.createXULElement("button");
+ itemACL.setAttribute("image", "chrome://tbsync/content/skin/acl_" + (folderData.getFolderProperty("downloadonly") ? "ro" : "rw") + ".png");
+ itemACL.setAttribute("class", "plain");
+ itemACL.setAttribute("style", "width: 35px; min-width: 35px; margin: 0; height:26px");
+ itemACL.setAttribute("updatefield", "acl");
+ if (roAttributes && rwAttributes) {
+ itemACL.setAttribute("type", "menu");
+ let menupopup = document.createXULElement("menupopup");
+ {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.downloadonly = false;
+ menuitem.setAttribute("class", "menuitem-iconic");
+ menuitem.setAttribute("image", "chrome://tbsync/content/skin/acl_rw2.png");
+ menuitem.addEventListener("command", this.updateReadOnly);
+ for (const [attr, value] of Object.entries(rwAttributes)) {
+ menuitem.setAttribute(attr, value);
+ }
+ menupopup.appendChild(menuitem);
+ }
+
+ {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.downloadonly = true;
+ menuitem.setAttribute("class", "menuitem-iconic");
+ menuitem.setAttribute("image", "chrome://tbsync/content/skin/acl_ro2.png");
+ menuitem.addEventListener("command", this.updateReadOnly);
+ for (const [attr, value] of Object.entries(roAttributes)) {
+ menuitem.setAttribute(attr, value);
+ }
+ menupopup.appendChild(menuitem);
+ }
+ itemACL.appendChild(menupopup);
+ }
+
+ //folder name
+ let itemLabel = document.createXULElement("description");
+ itemLabel.setAttribute("updatefield", "foldername");
+
+ //status
+ let itemStatus = document.createXULElement("description");
+ itemStatus.setAttribute("updatefield", "status");
+
+ //group1
+ let itemHGroup1 = document.createXULElement("hbox");
+ itemHGroup1.setAttribute("align", "center");
+ itemHGroup1.appendChild(itemSelCheckbox);
+ itemHGroup1.appendChild(itemType);
+ if (itemACL) itemHGroup1.appendChild(itemACL);
+
+ let itemVGroup1 = document.createXULElement("vbox");
+ //itemVGroup1.setAttribute("width", "93");
+ itemVGroup1.setAttribute("style", "width: 93px");
+ itemVGroup1.appendChild(itemHGroup1);
+
+ //group2
+ let itemHGroup2 = document.createXULElement("hbox");
+ itemHGroup2.setAttribute("align", "center");
+ itemHGroup2.setAttribute("style", "border: 1px center");
+ itemHGroup2.appendChild(itemLabel);
+
+ let itemVGroup2 = document.createXULElement("vbox");
+ //itemVGroup2.setAttribute("width", "150");
+ itemVGroup2.setAttribute("style", "padding: 3px; width: 150px");
+ itemVGroup2.appendChild(itemHGroup2);
+
+ //group3
+ let itemHGroup3 = document.createXULElement("hbox");
+ itemHGroup3.setAttribute("align", "center");
+ itemHGroup3.appendChild(itemStatus);
+
+ let itemVGroup3 = document.createXULElement("vbox");
+ //itemVGroup3.setAttribute("width", "250");
+ itemVGroup3.setAttribute("style", "padding: 3px; width: 250px");
+ itemVGroup3.appendChild(itemHGroup3);
+
+ //final row
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "min-height: 24px;");
+ row.appendChild(itemVGroup1);
+ row.appendChild(itemVGroup2);
+ row.appendChild(itemVGroup3);
+ return row;
+ }
+
+
+ /**
+ * ToggleFolder event
+ */
+ toggleFolder(event) {
+ let element = event.target;
+ let folderList = element.ownerDocument.getElementById("tbsync.accountsettings.folderlist");
+ if (folderList.selectedItem !== null && !folderList.disabled) {
+ // the folderData obj of the selected folder is attached to its row entry
+ let folder = folderList.selectedItem.folderData;
+
+ if (!folder.accountData.isEnabled())
+ return;
+
+ if (folder.getFolderProperty("selected")) {
+ // hasTarget() can throw an error, ignore that here
+ try {
+ if (!folder.targetData.hasTarget() || element.ownerDocument.defaultView.confirm(TbSync.getString("prompt.Unsubscribe"))) {
+ folder.targetData.removeTarget();
+ folder.setFolderProperty("selected", false);
+ } else {
+ if (element) {
+ //undo users action
+ element.setAttribute("checked", true);
+ }
+ }
+ } catch (e) {
+ folder.setFolderProperty("selected", false);
+ Components.utils.reportError(e);
+ }
+ } else {
+ //select and update status
+ folder.setFolderProperty("selected", true);
+ folder.setFolderProperty("status", "aborted");
+ folder.accountData.setAccountProperty("status", "notsyncronized");
+ }
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", folder.accountID);
+ }
+ }
+
+ /**
+ * updateReadOnly event
+ */
+ updateReadOnly(event) {
+ let element = event.target;
+ let folderList = element.ownerDocument.getElementById("tbsync.accountsettings.folderlist");
+ if (folderList.selectedItem !== null && !folderList.disabled) {
+ //the folderData obj of the selected folder is attached to its row entry
+ let folder = folderList.selectedItem.folderData;
+
+ //update value
+ let value = element.downloadonly;
+ folder.setFolderProperty("downloadonly", value);
+
+ //update icon
+ let button = element.parentNode.parentNode;
+ if (value) {
+ button.setAttribute('image','chrome://tbsync/content/skin/acl_ro.png');
+ } else {
+ button.setAttribute('image','chrome://tbsync/content/skin/acl_rw.png');
+ }
+
+ folder.targetData.setReadOnly(value);
+ }
+ }
+
+ /**
+ * Is called to update a row of the folderlist (the first cell is a select checkbox inserted by TbSync)
+ *
+ * @param document [in] document object of the account settings window
+ * @param listItem [in] the listitem of the row, which needs to be updated
+ * @param folderData [in] FolderData for that row
+ */
+ updateRow(document, listItem, folderData) {
+ let foldername = TbSync.providers[this.provider].StandardFolderList.getFolderDisplayName(folderData);
+ let status = folderData.getFolderStatus();
+ let selected = folderData.getFolderProperty("selected");
+
+ // get updatefields
+ let fields = {}
+ for (let f of listItem.querySelectorAll("[updatefield]")) {
+ fields[f.getAttribute("updatefield")] = f;
+ }
+
+ // update fields
+ fields.foldername.setAttribute("disabled", !selected);
+ fields.foldername.setAttribute("style", selected ? "" : "font-style:italic");
+ if (fields.foldername.textContent != foldername) {
+ fields.foldername.textContent = foldername;
+ fields.foldername.flex = "1";
+ }
+
+ fields.status.setAttribute("style", selected ? "" : "font-style:italic");
+ if (fields.status.textContent != status) {
+ fields.status.textContent = status;
+ fields.status.flex = "1";
+ }
+
+ if (fields.hasOwnProperty("acl")) {
+ fields.acl.setAttribute("image", "chrome://tbsync/content/skin/acl_" + (folderData.getFolderProperty("downloadonly") ? "ro" : "rw") + ".png");
+ fields.acl.setAttribute("disabled", folderData.accountData.isSyncing());
+ }
+
+ // update selectbox
+ let selbox = fields.selectbox;
+ if (selbox) {
+ if (folderData.getFolderProperty("selected")) {
+ selbox.setAttribute("checked", true);
+ } else {
+ selbox.removeAttribute("checked");
+ }
+
+ if (folderData.accountData.isSyncing()) {
+ selbox.setAttribute("disabled", true);
+ } else {
+ selbox.removeAttribute("disabled");
+ }
+ }
+ }
+}
diff --git a/content/modules/messenger.js b/content/modules/messenger.js
new file mode 100644
index 0000000..eb986c7
--- /dev/null
+++ b/content/modules/messenger.js
@@ -0,0 +1,99 @@
+/*
+ * 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 messenger = {
+
+ overlayManager : null,
+
+ load: async function () {
+ this.overlayManager = new OverlayManager(TbSync.extension, {verbose: 0});
+ await this.overlayManager.registerOverlay("chrome://messenger/content/messenger.xhtml", "chrome://tbsync/content/overlays/messenger.xhtml");
+ Services.obs.addObserver(this.initSyncObserver, "tbsync.observer.sync", false);
+ Services.obs.addObserver(this.syncstateObserver, "tbsync.observer.manager.updateSyncstate", false);
+
+ //inject overlays
+ this.overlayManager.startObserving();
+
+ },
+
+ unload: async function () {
+ //unload overlays
+ this.overlayManager.stopObserving();
+
+ Services.obs.removeObserver(this.initSyncObserver, "tbsync.observer.sync");
+ Services.obs.removeObserver(this.syncstateObserver, "tbsync.observer.manager.updateSyncstate");
+ },
+
+ // observer to catch changing syncstate and to update the status bar.
+ syncstateObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ //update status bar in all main windows
+ let windows = Services.wm.getEnumerator("mail:3pane");
+ while (windows.hasMoreElements()) {
+ let domWindow = windows.getNext();
+ if (TbSync) {
+ let status = domWindow.document.getElementById("tbsync.status");
+ if (status) {
+ let label = "TbSync: ";
+
+ if (TbSync.enabled) {
+
+ //check if any account is syncing, if not switch to idle
+ let accounts = TbSync.db.getAccounts();
+ let idle = true;
+ let err = false;
+
+ for (let i=0; i<accounts.allIDs.length && idle; i++) {
+ if (!accounts.IDs.includes(accounts.allIDs[i])) {
+ err = true;
+ continue;
+ }
+
+ //set idle to false, if at least one account is syncing
+ if (TbSync.core.isSyncing(accounts.allIDs[i])) idle = false;
+
+ //check for errors
+ switch (TbSync.db.getAccountProperty(accounts.allIDs[i], "status")) {
+ case "success":
+ case "disabled":
+ case "notsyncronized":
+ case "syncing":
+ break;
+ default:
+ err = true;
+ }
+ }
+
+ if (idle) {
+ if (err) label += TbSync.getString("info.error");
+ else label += TbSync.getString("info.idle");
+ } else {
+ label += TbSync.getString("status.syncing");
+ }
+ } else {
+ label += "Loading";
+ }
+ status.value = label;
+ }
+ }
+ }
+ }
+ },
+
+ // observer to init sync
+ initSyncObserver: {
+ observe: function (aSubject, aTopic, aData) {
+ if (TbSync.enabled) {
+ TbSync.core.syncAllAccounts();
+ } else {
+ //TbSync.manager.popupNotEnabled();
+ }
+ }
+ },
+}
diff --git a/content/modules/network.js b/content/modules/network.js
new file mode 100644
index 0000000..f5e81d3
--- /dev/null
+++ b/content/modules/network.js
@@ -0,0 +1,114 @@
+/*
+ * 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 network = {
+
+ load: async function () {
+ },
+
+ unload: async function () {
+ },
+
+ getContainerIdForUser: function(username) {
+ //define the allowed range of container ids to be used
+ let min = 10000;
+ let max = 19999;
+
+ //we need to store the container map in the main window, so it is persistent and survives a restart of this bootstrapped addon
+ //TODO: is there a better way to store this container map globally? Can there be TWO main windows?
+ let mainWindow = Services.wm.getMostRecentWindow("mail:3pane");
+
+ //init
+ if (!(mainWindow._containers)) {
+ mainWindow._containers = [];
+ }
+
+ //reset if adding an entry will exceed allowed range
+ if (mainWindow._containers.length > (max-min) && mainWindow._containers.indexOf(username) == -1) {
+ for (let i=0; i < mainWindow._containers.length; i++) {
+ //Services.clearData.deleteDataFromOriginAttributesPattern({ userContextId: i + min });
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data", JSON.stringify({ userContextId: i + min }));
+ }
+ mainWindow._containers = [];
+ }
+
+ let idx = mainWindow._containers.indexOf(username);
+ return (idx == -1) ? mainWindow._containers.push(username) - 1 + min : (idx + min);
+ },
+
+ resetContainerForUser: function(username) {
+ let id = this.getContainerIdForUser(username);
+ Services.obs.notifyObservers(null, "clear-origin-attributes-data", JSON.stringify({ userContextId: id }));
+ },
+
+ createTCPErrorFromFailedXHR: function (xhr) {
+ return this.createTCPErrorFromFailedRequest(xhr.channel.QueryInterface(Components.interfaces.nsIRequest));
+ },
+
+ createTCPErrorFromFailedRequest: function (request) {
+ //adapted from :
+ //https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
+ //codes: https://developer.mozilla.org/en-US/docs/Mozilla/Errors
+ let status = request.status;
+
+ if ((status & 0xff0000) === 0x5a0000) { // Security module
+ const nsINSSErrorsService = Components.interfaces.nsINSSErrorsService;
+ let nssErrorsService = Components.classes['@mozilla.org/nss_errors_service;1'].getService(nsINSSErrorsService);
+
+ // NSS_SEC errors (happen below the base value because of negative vals)
+ if ((status & 0xffff) < Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
+
+ // The bases are actually negative, so in our positive numeric space, we
+ // need to subtract the base off our value.
+ let nssErr = Math.abs(nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
+ switch (nssErr) {
+ case 11: return 'security::SEC_ERROR_EXPIRED_CERTIFICATE';
+ case 12: return 'security::SEC_ERROR_REVOKED_CERTIFICATE';
+ case 13: return 'security::SEC_ERROR_UNKNOWN_ISSUER';
+ case 20: return 'security::SEC_ERROR_UNTRUSTED_ISSUER';
+ case 21: return 'security::SEC_ERROR_UNTRUSTED_CERT';
+ case 36: return 'security::SEC_ERROR_CA_CERT_INVALID';
+ case 90: return 'security::SEC_ERROR_INADEQUATE_KEY_USAGE';
+ case 176: return 'security::SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED';
+ }
+ return 'security::UNKNOWN_SECURITY_ERROR';
+
+ } else {
+
+ // Calculating the difference
+ let sslErr = Math.abs(nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
+ switch (sslErr) {
+ case 3: return 'security::SSL_ERROR_NO_CERTIFICATE';
+ case 4: return 'security::SSL_ERROR_BAD_CERTIFICATE';
+ case 8: return 'security::SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE';
+ case 9: return 'security::SSL_ERROR_UNSUPPORTED_VERSION';
+ case 12: return 'security::SSL_ERROR_BAD_CERT_DOMAIN';
+ }
+ return 'security::UNKOWN_SSL_ERROR';
+
+ }
+
+ } else { //not the security module
+
+ switch (status) {
+ case 0x804B000C: return 'network::NS_ERROR_CONNECTION_REFUSED';
+ case 0x804B000E: return 'network::NS_ERROR_NET_TIMEOUT';
+ case 0x804B001E: return 'network::NS_ERROR_UNKNOWN_HOST';
+ case 0x804B0047: return 'network::NS_ERROR_NET_INTERRUPT';
+ case 0x805303F4: return 'network::NS_ERROR_DOM_BAD_URI';
+ // Custom error
+ case 0x804B002F: return 'network::REJECTED_REDIRECT_FROM_HTTPS_TO_HTTP';
+ }
+ return 'network::UNKNOWN_NETWORK_ERROR';
+
+ }
+ return null;
+ },
+}
diff --git a/content/modules/passwordManager.js b/content/modules/passwordManager.js
new file mode 100644
index 0000000..0145cd1
--- /dev/null
+++ b/content/modules/passwordManager.js
@@ -0,0 +1,78 @@
+/*
+ * 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 passwordManager = {
+
+ load: async function () {
+ },
+
+ unload: async function () {
+ },
+
+ removeLoginInfos: function(origin, realm, users = null) {
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Components.interfaces.nsILoginInfo, "init");
+
+ let logins = Services.logins.findLogins(origin, null, realm);
+ for (let i = 0; i < logins.length; i++) {
+ if (!users || users.includes(logins[i].username)) {
+ let currentLoginInfo = new nsLoginInfo(origin, null, realm, logins[i].username, logins[i].password, "", "");
+ try {
+ Services.logins.removeLogin(currentLoginInfo);
+ } catch (e) {
+ TbSync.dump("Error removing loginInfo", e);
+ }
+ }
+ }
+ },
+
+ updateLoginInfo: function(origin, realm, oldUser, newUser, newPassword) {
+ let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", Components.interfaces.nsILoginInfo, "init");
+
+ this.removeLoginInfos(origin, realm, [oldUser, newUser]);
+
+ let newLoginInfo = new nsLoginInfo(origin, null, realm, newUser, newPassword, "", "");
+ try {
+ Services.logins.addLogin(newLoginInfo);
+ } catch (e) {
+ TbSync.dump("Error adding loginInfo", e);
+ }
+ },
+
+ getLoginInfo: function(origin, realm, user) {
+ let logins = Services.logins.findLogins(origin, null, realm);
+ for (let i = 0; i < logins.length; i++) {
+ if (logins[i].username == user) {
+ return logins[i].password;
+ }
+ }
+ return null;
+ },
+
+
+ /** data obj
+ windowID
+ accountName
+ userName
+ userNameLocked
+
+ reference is an object in which an entry with windowID will be placed to hold a reference to the prompt window (so it can be closed externaly)
+ */
+ asyncPasswordPrompt: async function(data, reference) {
+ if (data.windowID) {
+ let url = "chrome://tbsync/content/passwordPrompt/passwordPrompt.xhtml";
+
+ return await new Promise(function(resolve, reject) {
+ reference[data.windowID] = TbSync.window.openDialog(url, "TbSyncPasswordPrompt:" + data.windowID, "centerscreen,chrome,resizable=no", data, resolve);
+ });
+ }
+
+ return false;
+ }
+}
diff --git a/content/modules/providers.js b/content/modules/providers.js
new file mode 100644
index 0000000..562b763
--- /dev/null
+++ b/content/modules/providers.js
@@ -0,0 +1,184 @@
+/*
+ * 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 providers = {
+
+ //list of default providers (available in add menu, even if not installed)
+ defaultProviders: {
+ "google" : {
+ name: "Google's People API",
+ homepageUrl: "https://addons.thunderbird.net/addon/google-4-tbsync/"},
+ "dav" : {
+ name: "CalDAV & CardDAV",
+ homepageUrl: "https://addons.thunderbird.net/addon/dav-4-tbsync/"},
+ "eas" : {
+ name: "Exchange ActiveSync",
+ homepageUrl: "https://addons.thunderbird.net/addon/eas-4-tbsync/"},
+ },
+
+ loadedProviders: null,
+
+ load: async function () {
+ this.loadedProviders = {};
+ },
+
+ unload: async function () {
+ for (let provider in this.loadedProviders) {
+ await this.unloadProvider(provider);
+ }
+ },
+
+
+
+
+
+ loadProvider: async function (extension, provider, js) {
+ //only load, if not yet loaded and if the provider name does not shadow a fuction inside provider.js
+ if (!this.loadedProviders.hasOwnProperty(provider) && !this.hasOwnProperty(provider) && js.startsWith("chrome://")) {
+ try {
+ let addon = await AddonManager.getAddonByID(extension.id);
+
+ //load provider subscripts into TbSync
+ this[provider] = {};
+ Services.scriptloader.loadSubScript(js, this[provider], "UTF-8");
+ if (TbSync.apiVersion != this[provider].Base.getApiVersion()) {
+ throw new Error("API version mismatch, TbSync@"+TbSync.apiVersion+" vs " + provider + "@" + this[provider].Base.getApiVersion());
+ }
+
+ this.loadedProviders[provider] = {
+ addon, extension,
+ addonId: extension.id,
+ version: addon.version.toString(),
+ createAccountWindow: null
+ };
+
+ addon.contributorsURL = this[provider].Base.getContributorsUrl();
+
+ // check if provider has its own implementation of folderList
+ if (!this[provider].hasOwnProperty("folderList")) this[provider].folderList = new TbSync.manager.FolderList(provider);
+
+ //load provider
+ await this[provider].Base.load();
+
+ await TbSync.messenger.overlayManager.registerOverlay("chrome://tbsync/content/manager/editAccount.xhtml?provider=" + provider, this[provider].Base.getEditAccountOverlayUrl());
+ TbSync.dump("Loaded provider", provider + "::" + this[provider].Base.getProviderName() + " ("+this.loadedProviders[provider].version+")");
+
+ // reset all accounts of this provider
+ let providerData = new TbSync.ProviderData(provider);
+ let accounts = providerData.getAllAccounts();
+ for (let accountData of accounts) {
+ // reset sync objects
+ TbSync.core.resetSyncDataObj(accountData.accountID);
+
+ // set all accounts which are syncing to notsyncronized
+ if (accountData.getAccountProperty("status") == "syncing") accountData.setAccountProperty("status", "notsyncronized");
+
+ // set each folder with PENDING status to ABORTED
+ let folders = TbSync.db.findFolders({"status": "pending"}, {"accountID": accountData.accountID});
+
+ for (let f=0; f < folders.length; f++) {
+ TbSync.db.setFolderProperty(folders[f].accountID, folders[f].folderID, "status", "aborted");
+ }
+ }
+
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateProviderList", provider);
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null);
+
+ // TB60 -> TB68 migration - remove icon and rename target if stale
+ for (let addressBook of MailServices.ab.directories) {
+ if (addressBook instanceof Components.interfaces.nsIAbDirectory) {
+ let storedProvider = TbSync.addressbook.getStringValue(addressBook, "tbSyncProvider", "");
+ if (provider == storedProvider && providerData.getFolders({"target": addressBook.UID}).length == 0) {
+ let name = addressBook.dirName;
+ addressBook.dirName = TbSync.getString("target.orphaned") + ": " + name;
+ addressBook.setStringValue("tbSyncIcon", "orphaned");
+ addressBook.setStringValue("tbSyncProvider", "orphaned");
+ addressBook.setStringValue("tbSyncAccountID", "");
+ }
+ }
+ }
+
+ let calManager = TbSync.lightning.cal.manager;
+ for (let calendar of calManager.getCalendars({})) {
+ let storedProvider = calendar.getProperty("tbSyncProvider");
+ if (provider == storedProvider && calendar.type == "storage" && providerData.getFolders({"target": calendar.id}).length == 0) {
+ let name = calendar.name;
+ calendar.name = TbSync.getString("target.orphaned") + ": " + name;
+ calendar.setProperty("disabled", true);
+ calendar.setProperty("tbSyncProvider", "orphaned");
+ calendar.setProperty("tbSyncAccountID", "");
+ }
+ }
+
+ } catch (e) {
+ delete this.loadedProviders[provider];
+ delete this[provider];
+ let info = new EventLogInfo(provider);
+ TbSync.eventlog.add("error", info, "FAILED to load provider <"+provider+">", e.message);
+ Components.utils.reportError(e);
+ }
+
+ }
+ },
+
+ unloadProvider: async function (provider) {
+ if (this.loadedProviders.hasOwnProperty(provider)) {
+ TbSync.dump("Unloading provider", provider);
+
+ if (this.loadedProviders[provider].createAccountWindow) {
+ this.loadedProviders[provider].createAccountWindow.close();
+ }
+
+ await this[provider].Base.unload();
+ delete this.loadedProviders[provider];
+ delete this[provider];
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateProviderList", provider);
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null);
+ }
+ },
+
+ getDefaultAccountEntries: function (provider) {
+ let defaults = TbSync.providers[provider].Base.getDefaultAccountEntries();
+
+ // List of default system account properties.
+ // Do not remove search marker for doc.
+ // DefaultAccountPropsStart
+ defaults.provider = provider;
+ defaults.accountID = "";
+ defaults.lastsynctime = 0;
+ defaults.status = "disabled";
+ defaults.autosync = 0;
+ defaults.noAutosyncUntil = 0;
+ defaults.accountname = "";
+ // DefaultAccountPropsEnd
+
+ return defaults;
+ },
+
+ getDefaultFolderEntries: function (accountID) {
+ let provider = TbSync.db.getAccountProperty(accountID, "provider");
+ let defaults = TbSync.providers[provider].Base.getDefaultFolderEntries();
+
+ // List of default system folder properties.
+ // Do not remove search marker for doc.
+ // DefaultFolderPropsStart
+ defaults.accountID = accountID;
+ defaults.targetType = "";
+ defaults.cached = false;
+ defaults.selected = false;
+ defaults.lastsynctime = 0;
+ defaults.status = "";
+ defaults.foldername = "";
+ defaults.downloadonly = false;
+ // DefaultFolderPropsEnd
+
+ return defaults;
+ },
+}
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, '');
+}
diff --git a/content/modules/tools.js b/content/modules/tools.js
new file mode 100644
index 0000000..fe406b9
--- /dev/null
+++ b/content/modules/tools.js
@@ -0,0 +1,80 @@
+/*
+ * 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 tools = {
+
+ load: async function () {
+ },
+
+ unload: async function () {
+ },
+
+ // async sleep function using Promise to postpone actions to keep UI responsive
+ sleep : function (_delay, useRequestIdleCallback = false) {
+ let useIdleCallback = false;
+ let delay = 5;//_delay;
+ if (TbSync.window.requestIdleCallback && useRequestIdleCallback) {
+ useIdleCallback = true;
+ delay= 2;
+ }
+ let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+
+ return new Promise(function(resolve, reject) {
+ let event = {
+ notify: function(timer) {
+ if (useIdleCallback) {
+ TbSync.window.requestIdleCallback(resolve);
+ } else {
+ resolve();
+ }
+ }
+ }
+ timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ });
+ },
+
+ // this is derived from: http://jonisalonen.com/2012/from-utf-16-to-utf-8-in-javascript/
+ // javascript strings are utf16, btoa needs utf8 , so we need to encode
+ toUTF8: function (str) {
+ var utf8 = "";
+ for (var i=0; i < str.length; i++) {
+ var charcode = str.charCodeAt(i);
+ if (charcode < 0x80) utf8 += String.fromCharCode(charcode);
+ else if (charcode < 0x800) {
+ utf8 += String.fromCharCode(0xc0 | (charcode >> 6),
+ 0x80 | (charcode & 0x3f));
+ }
+ else if (charcode < 0xd800 || charcode >= 0xe000) {
+ utf8 += String.fromCharCode(0xe0 | (charcode >> 12),
+ 0x80 | ((charcode>>6) & 0x3f),
+ 0x80 | (charcode & 0x3f));
+ }
+
+ // surrogate pair
+ else {
+ i++;
+ // UTF-16 encodes 0x10000-0x10FFFF by
+ // subtracting 0x10000 and splitting the
+ // 20 bits of 0x0-0xFFFFF into two halves
+ charcode = 0x10000 + (((charcode & 0x3ff)<<10)
+ | (str.charCodeAt(i) & 0x3ff))
+ utf8 += String.fromCharCode(0xf0 | (charcode >>18),
+ 0x80 | ((charcode>>12) & 0x3f),
+ 0x80 | ((charcode>>6) & 0x3f),
+ 0x80 | (charcode & 0x3f));
+ }
+ }
+ return utf8;
+ },
+
+ b64encode: function (str) {
+ return btoa(this.toUTF8(str));
+ }
+}
diff --git a/content/overlays/messenger.js b/content/overlays/messenger.js
new file mode 100644
index 0000000..b6d0a97
--- /dev/null
+++ b/content/overlays/messenger.js
@@ -0,0 +1,22 @@
+/*
+ * 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncMessenger = {
+
+ onInject: function (window) {
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null);
+ },
+
+ onRemove: function (window) {
+ },
+
+};
diff --git a/content/overlays/messenger.xhtml b/content/overlays/messenger.xhtml
new file mode 100644
index 0000000..37d07ca
--- /dev/null
+++ b/content/overlays/messenger.xhtml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<overlay
+ id="TbSyncMessengerOverlay"
+ omscope="tbSyncMessenger"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://tbsync/content/overlays/messenger.js" />
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+
+ <menupopup id="tbsync.statusmenu">
+ <menuitem label="__TBSYNCMSG_popup.opensettings__" oncommand="TbSync.manager.openManagerWindow(event);" />
+ </menupopup>
+
+ <label id="tbsync.status" class="statusbarpanel" value="TbSync" onclick="TbSync.manager.openManagerWindow(event);" appendto="status-bar"/>
+
+ <!--menuitem id="tbsync.taskPopupEntry" label="__TBSYNCMSG_menu.settingslabel__" appendto="taskPopup" onclick="TbSync.manager.openManagerWindow(event);" / -->
+ <menuitem id="tbsync.accountmgrEntry" label="__TBSYNCMSG_menu.settingslabel__" insertbefore="menu_accountmgr" oncommand="TbSync.manager.openManagerWindow(event);" />
+
+</overlay>
diff --git a/content/passwordPrompt/passwordPrompt.css b/content/passwordPrompt/passwordPrompt.css
new file mode 100644
index 0000000..4527ac3
--- /dev/null
+++ b/content/passwordPrompt/passwordPrompt.css
@@ -0,0 +1,13 @@
+ .grid-container {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: auto auto auto;
+ gap: 1px;
+ width: 250px;
+ margin: 2ex auto;
+ }
+
+ .grid-item {
+ padding: 2px;
+ text-align: left;
+ }
diff --git a/content/passwordPrompt/passwordPrompt.js b/content/passwordPrompt/passwordPrompt.js
new file mode 100644
index 0000000..8af40d9
--- /dev/null
+++ b/content/passwordPrompt/passwordPrompt.js
@@ -0,0 +1,47 @@
+/*
+ * 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 tbSyncPassword = {
+
+ onload: function () {
+ let data = window.arguments[0];
+ this.resolve = window.arguments[1];
+ this.resolved = false;
+
+ this.namefield = document.getElementById("tbsync.account");
+ this.passfield = document.getElementById("tbsync.password");
+ this.userfield = document.getElementById("tbsync.user");
+
+ this.namefield.value = data.accountname;
+ this.userfield.value = data.username;
+ this.userfield.disabled = data.usernameLocked;
+
+ window.addEventListener("unload", tbSyncPassword.doCANCEL.bind(this));
+ document.getElementById("tbsync.password").focus();
+ document.getElementById("tbsync.password.ok").addEventListener("click", tbSyncPassword.doOK.bind(this));
+ document.getElementById("tbsync.password.cancel").addEventListener("click", () => window.close());
+ },
+
+ doOK: function (event) {
+ if (!this.resolved) {
+ this.resolved = true
+ this.resolve({username: this.userfield.value, password: this.passfield.value});
+ window.close();
+ }
+ },
+
+ doCANCEL: function (event) {
+ if (!this.resolved) {
+ this.resolved = true
+ this.resolve(false);
+ }
+ },
+
+};
diff --git a/content/passwordPrompt/passwordPrompt.xhtml b/content/passwordPrompt/passwordPrompt.xhtml
new file mode 100644
index 0000000..fc554b5
--- /dev/null
+++ b/content/passwordPrompt/passwordPrompt.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://tbsync/content/passwordPrompt/passwordPrompt.css" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="__TBSYNCMSG_password.title__"
+ onload="tbSyncPassword.onload();">
+
+ <script type="application/javascript" src="chrome://tbsync/content/passwordPrompt/passwordPrompt.js"/>
+ <script type="text/javascript" src="chrome://tbsync/content/scripts/locales.js" />
+
+ <vbox flex="1">
+ <description style="padding: 5px; width: 350px;">__TBSYNCMSG_password.description__</description>
+
+ <html:div class="grid-container">
+ <html:div class="grid-item"><label value="__TBSYNCMSG_password.account__"/></html:div>
+ <html:div class="grid-item"><label class="header" id="tbsync.account" /></html:div>
+ <html:div class="grid-item"><label value="__TBSYNCMSG_password.user__"/></html:div>
+ <html:div class="grid-item"><html:input id="tbsync.user" /></html:div>
+ <html:div class="grid-item"><label value="__TBSYNCMSG_password.password__"/></html:div>
+ <html:div class="grid-item"><html:input type="password" id="tbsync.password"/></html:div>
+ </html:div>
+ <hbox style="padding: 5px">
+ <vbox flex="1"></vbox>
+ <button id="tbsync.password.ok" label="__TBSYNCMSG_password.ok__" />
+ <button id="tbsync.password.cancel" label="__TBSYNCMSG_password.cancel__" />
+ </hbox>
+ </vbox>
+
+</window>
diff --git a/content/scripts/bootstrap.js b/content/scripts/bootstrap.js
new file mode 100644
index 0000000..706946f
--- /dev/null
+++ b/content/scripts/bootstrap.js
@@ -0,0 +1,89 @@
+/*
+ * 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/.
+ */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function startup(data, reason) {
+ // possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ // set default prefs
+ let defaults = Services.prefs.getDefaultBranch("extensions.tbsync.");
+ defaults.setBoolPref("debug.testoptions", false);
+ defaults.setBoolPref("log.toconsole", false);
+ defaults.setIntPref("log.userdatalevel", 0); //0 - off 1 - userdata only on errors 2 - including full userdata, 3 - extra infos
+
+ // Check if at least one main window has finished loading
+ let windows = Services.wm.getEnumerator("mail:3pane");
+ if (windows.hasMoreElements()) {
+ let domWindow = windows.getNext();
+ WindowListener.loadIntoWindow(domWindow);
+ }
+
+ // Wait for any new windows to open.
+ Services.wm.addListener(WindowListener);
+
+ //DO NOT ADD ANYTHING HERE!
+}
+
+function shutdown(data, reason) {
+ //possible reasons: APP_SHUTDOWN, ADDON_DISABLE, ADDON_UNINSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE
+
+ // Stop listening for any new windows to open.
+ Services.wm.removeListener(WindowListener);
+
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ TbSync.enabled = false;
+ TbSync.unload().then(function() {
+ Cu.unload("chrome://tbsync/content/tbsync.jsm");
+ Cu.unload("chrome://tbsync/content/HttpRequest.jsm");
+ Cu.unload("chrome://tbsync/content/OverlayManager.jsm");
+ // HACK WARNING:
+ // - the Addon Manager does not properly clear all addon related caches on update;
+ // - in order to fully update images and locales, their caches need clearing here
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+ });
+}
+
+
+var WindowListener = {
+
+ async loadIntoWindow(window) {
+ if (window.document.readyState != "complete") {
+ // Make sure the window load has completed.
+ await new Promise(resolve => {
+ window.addEventListener("load", resolve, { once: true });
+ });
+ }
+
+ // Check if the opened window is the one we want to modify.
+ if (window.document.documentElement.getAttribute("windowtype") === "mail:3pane") {
+ // the main window has loaded, continue with init
+ var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+ if (!TbSync.enabled) TbSync.load(window, addon, extension);
+ }
+ },
+
+
+ unloadFromWindow(window) {
+ },
+
+ // nsIWindowMediatorListener functions
+ onOpenWindow(xulWindow) {
+ // A new window has opened.
+ let domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ // The domWindow.document.documentElement.getAttribute("windowtype") is not set before the load, so we cannot check it here
+ this.loadIntoWindow(domWindow);
+ },
+
+ onCloseWindow(xulWindow) {
+ },
+
+ onWindowTitleChange(xulWindow, newTitle) {
+ },
+};
diff --git a/content/scripts/locales.js b/content/scripts/locales.js
new file mode 100644
index 0000000..806c4f2
--- /dev/null
+++ b/content/scripts/locales.js
@@ -0,0 +1,3 @@
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+TbSync.localizeOnLoad(window);
diff --git a/content/skin/ab.css b/content/skin/ab.css
new file mode 100644
index 0000000..5c8169b
--- /dev/null
+++ b/content/skin/ab.css
@@ -0,0 +1,8 @@
+treechildren::-moz-tree-image(DirCol, orphaned) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://tbsync/content/skin/error16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="orphaned"] {
+ list-style-image: url("chrome://tbsync/content/skin/error16.png");
+}
diff --git a/content/skin/acl_ro.png b/content/skin/acl_ro.png
new file mode 100644
index 0000000..ee806fc
--- /dev/null
+++ b/content/skin/acl_ro.png
Binary files differ
diff --git a/content/skin/acl_ro2.png b/content/skin/acl_ro2.png
new file mode 100644
index 0000000..f3f1de9
--- /dev/null
+++ b/content/skin/acl_ro2.png
Binary files differ
diff --git a/content/skin/acl_rw.png b/content/skin/acl_rw.png
new file mode 100644
index 0000000..109fb45
--- /dev/null
+++ b/content/skin/acl_rw.png
Binary files differ
diff --git a/content/skin/acl_rw2.png b/content/skin/acl_rw2.png
new file mode 100644
index 0000000..1b799fd
--- /dev/null
+++ b/content/skin/acl_rw2.png
Binary files differ
diff --git a/content/skin/add16.png b/content/skin/add16.png
new file mode 100644
index 0000000..b366b96
--- /dev/null
+++ b/content/skin/add16.png
Binary files differ
diff --git a/content/skin/browserOverlay.css b/content/skin/browserOverlay.css
new file mode 100644
index 0000000..3c09214
--- /dev/null
+++ b/content/skin/browserOverlay.css
@@ -0,0 +1,10 @@
+/* skin/toolbar-button.css */
+
+#tbsync-toolbarbutton {
+ list-style-image: url("chrome://tbsync/content/skin/sync.png");
+}
+
+toolbar[iconsize="small"] #tbsync-toolbarbutton {
+ list-style-image: url("chrome://tbsync/content/skin/syncsmall.png");
+}
+
diff --git a/content/skin/calendar16.png b/content/skin/calendar16.png
new file mode 100644
index 0000000..2f7f575
--- /dev/null
+++ b/content/skin/calendar16.png
Binary files differ
diff --git a/content/skin/calendar16_shared.png b/content/skin/calendar16_shared.png
new file mode 100644
index 0000000..c496c40
--- /dev/null
+++ b/content/skin/calendar16_shared.png
Binary files differ
diff --git a/content/skin/catman32.png b/content/skin/catman32.png
new file mode 100644
index 0000000..f87b71e
--- /dev/null
+++ b/content/skin/catman32.png
Binary files differ
diff --git a/content/skin/connect16.png b/content/skin/connect16.png
new file mode 100644
index 0000000..7af930e
--- /dev/null
+++ b/content/skin/connect16.png
Binary files differ
diff --git a/content/skin/contacts16.png b/content/skin/contacts16.png
new file mode 100644
index 0000000..101a9d3
--- /dev/null
+++ b/content/skin/contacts16.png
Binary files differ
diff --git a/content/skin/contacts16_shared.png b/content/skin/contacts16_shared.png
new file mode 100644
index 0000000..4a5d049
--- /dev/null
+++ b/content/skin/contacts16_shared.png
Binary files differ
diff --git a/content/skin/del16.png b/content/skin/del16.png
new file mode 100644
index 0000000..8db349f
--- /dev/null
+++ b/content/skin/del16.png
Binary files differ
diff --git a/content/skin/disabled16.png b/content/skin/disabled16.png
new file mode 100644
index 0000000..ee10cd0
--- /dev/null
+++ b/content/skin/disabled16.png
Binary files differ
diff --git a/content/skin/eas16.png b/content/skin/eas16.png
new file mode 100644
index 0000000..072d5f0
--- /dev/null
+++ b/content/skin/eas16.png
Binary files differ
diff --git a/content/skin/error16.png b/content/skin/error16.png
new file mode 100644
index 0000000..5177258
--- /dev/null
+++ b/content/skin/error16.png
Binary files differ
diff --git a/content/skin/fix_dropdown_1534697.css b/content/skin/fix_dropdown_1534697.css
new file mode 100644
index 0000000..c5e7606
--- /dev/null
+++ b/content/skin/fix_dropdown_1534697.css
@@ -0,0 +1,4 @@
+button.plain .button-menu-dropmarker {
+ -moz-appearance: toolbarbutton-dropdown;
+ display: block;
+}
diff --git a/content/skin/group32.png b/content/skin/group32.png
new file mode 100644
index 0000000..a58f920
--- /dev/null
+++ b/content/skin/group32.png
Binary files differ
diff --git a/content/skin/help32.png b/content/skin/help32.png
new file mode 100644
index 0000000..db2141a
--- /dev/null
+++ b/content/skin/help32.png
Binary files differ
diff --git a/content/skin/info16.png b/content/skin/info16.png
new file mode 100644
index 0000000..eabaf75
--- /dev/null
+++ b/content/skin/info16.png
Binary files differ
diff --git a/content/skin/lock24.png b/content/skin/lock24.png
new file mode 100644
index 0000000..d0e07ab
--- /dev/null
+++ b/content/skin/lock24.png
Binary files differ
diff --git a/content/skin/provider16.png b/content/skin/provider16.png
new file mode 100644
index 0000000..d7ddcb8
--- /dev/null
+++ b/content/skin/provider16.png
Binary files differ
diff --git a/content/skin/provider32.png b/content/skin/provider32.png
new file mode 100644
index 0000000..4a1d0a9
--- /dev/null
+++ b/content/skin/provider32.png
Binary files differ
diff --git a/content/skin/report_open.png b/content/skin/report_open.png
new file mode 100644
index 0000000..5c0f4cb
--- /dev/null
+++ b/content/skin/report_open.png
Binary files differ
diff --git a/content/skin/report_send.png b/content/skin/report_send.png
new file mode 100644
index 0000000..8dcc893
--- /dev/null
+++ b/content/skin/report_send.png
Binary files differ
diff --git a/content/skin/settings32.png b/content/skin/settings32.png
new file mode 100644
index 0000000..9c6a889
--- /dev/null
+++ b/content/skin/settings32.png
Binary files differ
diff --git a/content/skin/slider-off.png b/content/skin/slider-off.png
new file mode 100644
index 0000000..8243d81
--- /dev/null
+++ b/content/skin/slider-off.png
Binary files differ
diff --git a/content/skin/slider-on.png b/content/skin/slider-on.png
new file mode 100644
index 0000000..c9dd28e
--- /dev/null
+++ b/content/skin/slider-on.png
Binary files differ
diff --git a/content/skin/spinner.gif b/content/skin/spinner.gif
new file mode 100644
index 0000000..5b33f7e
--- /dev/null
+++ b/content/skin/spinner.gif
Binary files differ
diff --git a/content/skin/src/LICENSE b/content/skin/src/LICENSE
new file mode 100644
index 0000000..f8ad367
--- /dev/null
+++ b/content/skin/src/LICENSE
@@ -0,0 +1,131 @@
+The following files are released into PUBLIC DOMAIN.
+
+slider.xcf
+slider-on.png
+slider-off.png
+
+The png files are based on the xcf file.
+
+
+
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work. \ No newline at end of file
diff --git a/content/skin/src/slider-off.png b/content/skin/src/slider-off.png
new file mode 100644
index 0000000..8243d81
--- /dev/null
+++ b/content/skin/src/slider-off.png
Binary files differ
diff --git a/content/skin/src/slider-on.png b/content/skin/src/slider-on.png
new file mode 100644
index 0000000..c9dd28e
--- /dev/null
+++ b/content/skin/src/slider-on.png
Binary files differ
diff --git a/content/skin/src/slider.xcf b/content/skin/src/slider.xcf
new file mode 100644
index 0000000..cca28cd
--- /dev/null
+++ b/content/skin/src/slider.xcf
Binary files differ
diff --git a/content/skin/sync16.png b/content/skin/sync16.png
new file mode 100644
index 0000000..87e3ee7
--- /dev/null
+++ b/content/skin/sync16.png
Binary files differ
diff --git a/content/skin/sync16_1.png b/content/skin/sync16_1.png
new file mode 100644
index 0000000..679d5d0
--- /dev/null
+++ b/content/skin/sync16_1.png
Binary files differ
diff --git a/content/skin/sync16_2.png b/content/skin/sync16_2.png
new file mode 100644
index 0000000..49fe7c8
--- /dev/null
+++ b/content/skin/sync16_2.png
Binary files differ
diff --git a/content/skin/sync16_3.png b/content/skin/sync16_3.png
new file mode 100644
index 0000000..87e3ee7
--- /dev/null
+++ b/content/skin/sync16_3.png
Binary files differ
diff --git a/content/skin/sync16_4.png b/content/skin/sync16_4.png
new file mode 100644
index 0000000..94cd442
--- /dev/null
+++ b/content/skin/sync16_4.png
Binary files differ
diff --git a/content/skin/tbsync.png b/content/skin/tbsync.png
new file mode 100644
index 0000000..be77a1f
--- /dev/null
+++ b/content/skin/tbsync.png
Binary files differ
diff --git a/content/skin/tbsync64.png b/content/skin/tbsync64.png
new file mode 100644
index 0000000..0877be0
--- /dev/null
+++ b/content/skin/tbsync64.png
Binary files differ
diff --git a/content/skin/tick16.png b/content/skin/tick16.png
new file mode 100644
index 0000000..db93138
--- /dev/null
+++ b/content/skin/tick16.png
Binary files differ
diff --git a/content/skin/todo16.png b/content/skin/todo16.png
new file mode 100644
index 0000000..0023b3a
--- /dev/null
+++ b/content/skin/todo16.png
Binary files differ
diff --git a/content/skin/todo16_share.png b/content/skin/todo16_share.png
new file mode 100644
index 0000000..46849be
--- /dev/null
+++ b/content/skin/todo16_share.png
Binary files differ
diff --git a/content/skin/trash16.png b/content/skin/trash16.png
new file mode 100644
index 0000000..b8c10d3
--- /dev/null
+++ b/content/skin/trash16.png
Binary files differ
diff --git a/content/skin/update32.png b/content/skin/update32.png
new file mode 100644
index 0000000..f9621e8
--- /dev/null
+++ b/content/skin/update32.png
Binary files differ
diff --git a/content/skin/user48.png b/content/skin/user48.png
new file mode 100644
index 0000000..51bf808
--- /dev/null
+++ b/content/skin/user48.png
Binary files differ
diff --git a/content/skin/warning16.png b/content/skin/warning16.png
new file mode 100644
index 0000000..3f04b67
--- /dev/null
+++ b/content/skin/warning16.png
Binary files differ
diff --git a/content/tbsync.jsm b/content/tbsync.jsm
new file mode 100644
index 0000000..e31931d
--- /dev/null
+++ b/content/tbsync.jsm
@@ -0,0 +1,163 @@
+/*
+ * 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 EXPORTED_SYMBOLS = ["TbSync"];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { OverlayManager } = ChromeUtils.import("chrome://tbsync/content/OverlayManager.jsm");
+
+var TbSync = {
+
+ enabled: false,
+ shutdown: false,
+
+ window: null,
+ addon: null,
+ version: 0,
+ debugMode: false,
+ apiVersion: "2.5",
+
+ prefs: Services.prefs.getBranch("extensions.tbsync."),
+
+ decoder: new TextDecoder(),
+ encoder: new TextEncoder(),
+
+ modules : [],
+ extension : null,
+
+ // global load
+ load: async function (window, addon, extension) {
+ //public module and IO module needs to be loaded beforehand
+ Services.scriptloader.loadSubScript("chrome://tbsync/content/modules/public.js", this, "UTF-8");
+ Services.scriptloader.loadSubScript("chrome://tbsync/content/modules/io.js", this, "UTF-8");
+
+ //clear debug log on start
+ this.io.initFile("debug.log");
+
+ this.window = window;
+ this.addon = addon;
+ this.addon.contributorsURL = "https://github.com/jobisoft/TbSync/blob/master/CONTRIBUTORS.md";
+ this.extension = extension;
+ this.dump("TbSync init","Start (" + this.addon.version.toString() + ")");
+
+ //print information about Thunderbird version and OS
+ this.dump(Services.appinfo.name, Services.appinfo.version + " on " + Services.appinfo.OS);
+
+ // register modules to be used by TbSync
+ this.modules.push({name: "db", state: 0});
+ this.modules.push({name: "addressbook", state: 0});
+ this.modules.push({name: "lightning", state: 0});
+ this.modules.push({name: "eventlog", state: 0});
+ this.modules.push({name: "core", state: 0});
+ this.modules.push({name: "passwordManager", state: 0});
+ this.modules.push({name: "network", state: 0});
+ this.modules.push({name: "tools", state: 0});
+ this.modules.push({name: "manager", state: 0});
+ this.modules.push({name: "providers", state: 0});
+ this.modules.push({name: "messenger", state: 0});
+
+ //load modules
+ for (let module of this.modules) {
+ try {
+ Services.scriptloader.loadSubScript("chrome://tbsync/content/modules/" + module.name + ".js", this, "UTF-8");
+ module.state = 1;
+ this.dump("Loading module <" + module.name + ">", "OK");
+ } catch (e) {
+ this.dump("Loading module <" + module.name + ">", "FAILED!");
+ Components.utils.reportError(e);
+ }
+ }
+
+ //call init function of loaded modules
+ for (let module of this.modules) {
+ if (module.state == 1) {
+ try {
+ this.dump("Initializing module", "<" + module.name + ">");
+ await this[module.name].load();
+ module.state = 2;
+ } catch (e) {
+ this.dump("Initialization of module <" + module.name + "> FAILED", e.message + "\n" + e.stack);
+ Components.utils.reportError(e);
+ }
+ }
+ }
+
+ //was debug mode enabled during startup?
+ this.debugMode = (this.prefs.getIntPref("log.userdatalevel") > 0);
+
+ //enable TbSync
+ this.enabled = true;
+
+ //notify about finished init of TbSync
+ Services.obs.notifyObservers(null, "tbsync.observer.manager.updateSyncstate", null);
+ Services.obs.notifyObservers(null, 'tbsync.observer.initialized', null);
+
+ //activate sync timer
+ this.syncTimer.start();
+
+ this.dump("TbSync init","Done");
+ },
+
+ // global unload
+ unload: async function() {
+ //cancel sync timer
+ this.syncTimer.cancel();
+
+ //unload modules in reverse order
+ this.modules.reverse();
+ for (let module of this.modules) {
+ if (module.state == 2) {
+ try {
+ await this[module.name].unload();
+ this.dump("Unloading module <" + module.name + ">", "OK");
+ } catch (e) {
+ this.dump("Unloading module <" + module.name + ">", "FAILED!");
+ Components.utils.reportError(e);
+ }
+ }
+ }
+ },
+
+ // timer for periodic sync
+ syncTimer: {
+ timer: Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer),
+
+ start: function () {
+ this.timer.cancel();
+ this.timer.initWithCallback(this.event, 60000, 3); //run timer every 60s
+ },
+
+ cancel: function () {
+ this.timer.cancel();
+ },
+
+ event: {
+ notify: function (timer) {
+ if (TbSync.enabled) {
+ //get all accounts and check, which one needs sync
+ let accounts = TbSync.db.getAccounts();
+ for (let i=0; i<accounts.IDs.length; i++) {
+ let now = Date.now();
+ let syncInterval = accounts.data[accounts.IDs[i]].autosync * 60 * 1000;
+ let lastsynctime = accounts.data[accounts.IDs[i]].lastsynctime;
+ let noAutosyncUntil = accounts.data[accounts.IDs[i]].noAutosyncUntil || 0;
+ if (TbSync.core.isEnabled(accounts.IDs[i]) && (syncInterval > 0) && (now > (lastsynctime + syncInterval)) && (now > noAutosyncUntil)) {
+ TbSync.core.syncAccount(accounts.IDs[i]);
+ }
+ }
+ }
+ }
+ }
+ }
+};