diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 06:29:37 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 06:29:37 +0000 |
commit | 9355e23a909a7801b3ccdf68ee05b3480be42407 (patch) | |
tree | 78220623341b88bd42d16929e2acb3e5ef52e0c8 /content | |
parent | Initial commit. (diff) | |
download | tbsync-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 '')
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 Binary files differnew file mode 100644 index 0000000..ee806fc --- /dev/null +++ b/content/skin/acl_ro.png diff --git a/content/skin/acl_ro2.png b/content/skin/acl_ro2.png Binary files differnew file mode 100644 index 0000000..f3f1de9 --- /dev/null +++ b/content/skin/acl_ro2.png diff --git a/content/skin/acl_rw.png b/content/skin/acl_rw.png Binary files differnew file mode 100644 index 0000000..109fb45 --- /dev/null +++ b/content/skin/acl_rw.png diff --git a/content/skin/acl_rw2.png b/content/skin/acl_rw2.png Binary files differnew file mode 100644 index 0000000..1b799fd --- /dev/null +++ b/content/skin/acl_rw2.png diff --git a/content/skin/add16.png b/content/skin/add16.png Binary files differnew file mode 100644 index 0000000..b366b96 --- /dev/null +++ b/content/skin/add16.png 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 Binary files differnew file mode 100644 index 0000000..2f7f575 --- /dev/null +++ b/content/skin/calendar16.png diff --git a/content/skin/calendar16_shared.png b/content/skin/calendar16_shared.png Binary files differnew file mode 100644 index 0000000..c496c40 --- /dev/null +++ b/content/skin/calendar16_shared.png diff --git a/content/skin/catman32.png b/content/skin/catman32.png Binary files differnew file mode 100644 index 0000000..f87b71e --- /dev/null +++ b/content/skin/catman32.png diff --git a/content/skin/connect16.png b/content/skin/connect16.png Binary files differnew file mode 100644 index 0000000..7af930e --- /dev/null +++ b/content/skin/connect16.png diff --git a/content/skin/contacts16.png b/content/skin/contacts16.png Binary files differnew file mode 100644 index 0000000..101a9d3 --- /dev/null +++ b/content/skin/contacts16.png diff --git a/content/skin/contacts16_shared.png b/content/skin/contacts16_shared.png Binary files differnew file mode 100644 index 0000000..4a5d049 --- /dev/null +++ b/content/skin/contacts16_shared.png diff --git a/content/skin/del16.png b/content/skin/del16.png Binary files differnew file mode 100644 index 0000000..8db349f --- /dev/null +++ b/content/skin/del16.png diff --git a/content/skin/disabled16.png b/content/skin/disabled16.png Binary files differnew file mode 100644 index 0000000..ee10cd0 --- /dev/null +++ b/content/skin/disabled16.png diff --git a/content/skin/eas16.png b/content/skin/eas16.png Binary files differnew file mode 100644 index 0000000..072d5f0 --- /dev/null +++ b/content/skin/eas16.png diff --git a/content/skin/error16.png b/content/skin/error16.png Binary files differnew file mode 100644 index 0000000..5177258 --- /dev/null +++ b/content/skin/error16.png 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 Binary files differnew file mode 100644 index 0000000..a58f920 --- /dev/null +++ b/content/skin/group32.png diff --git a/content/skin/help32.png b/content/skin/help32.png Binary files differnew file mode 100644 index 0000000..db2141a --- /dev/null +++ b/content/skin/help32.png diff --git a/content/skin/info16.png b/content/skin/info16.png Binary files differnew file mode 100644 index 0000000..eabaf75 --- /dev/null +++ b/content/skin/info16.png diff --git a/content/skin/lock24.png b/content/skin/lock24.png Binary files differnew file mode 100644 index 0000000..d0e07ab --- /dev/null +++ b/content/skin/lock24.png diff --git a/content/skin/provider16.png b/content/skin/provider16.png Binary files differnew file mode 100644 index 0000000..d7ddcb8 --- /dev/null +++ b/content/skin/provider16.png diff --git a/content/skin/provider32.png b/content/skin/provider32.png Binary files differnew file mode 100644 index 0000000..4a1d0a9 --- /dev/null +++ b/content/skin/provider32.png diff --git a/content/skin/report_open.png b/content/skin/report_open.png Binary files differnew file mode 100644 index 0000000..5c0f4cb --- /dev/null +++ b/content/skin/report_open.png diff --git a/content/skin/report_send.png b/content/skin/report_send.png Binary files differnew file mode 100644 index 0000000..8dcc893 --- /dev/null +++ b/content/skin/report_send.png diff --git a/content/skin/settings32.png b/content/skin/settings32.png Binary files differnew file mode 100644 index 0000000..9c6a889 --- /dev/null +++ b/content/skin/settings32.png diff --git a/content/skin/slider-off.png b/content/skin/slider-off.png Binary files differnew file mode 100644 index 0000000..8243d81 --- /dev/null +++ b/content/skin/slider-off.png diff --git a/content/skin/slider-on.png b/content/skin/slider-on.png Binary files differnew file mode 100644 index 0000000..c9dd28e --- /dev/null +++ b/content/skin/slider-on.png diff --git a/content/skin/spinner.gif b/content/skin/spinner.gif Binary files differnew file mode 100644 index 0000000..5b33f7e --- /dev/null +++ b/content/skin/spinner.gif 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 Binary files differnew file mode 100644 index 0000000..8243d81 --- /dev/null +++ b/content/skin/src/slider-off.png diff --git a/content/skin/src/slider-on.png b/content/skin/src/slider-on.png Binary files differnew file mode 100644 index 0000000..c9dd28e --- /dev/null +++ b/content/skin/src/slider-on.png diff --git a/content/skin/src/slider.xcf b/content/skin/src/slider.xcf Binary files differnew file mode 100644 index 0000000..cca28cd --- /dev/null +++ b/content/skin/src/slider.xcf diff --git a/content/skin/sync16.png b/content/skin/sync16.png Binary files differnew file mode 100644 index 0000000..87e3ee7 --- /dev/null +++ b/content/skin/sync16.png diff --git a/content/skin/sync16_1.png b/content/skin/sync16_1.png Binary files differnew file mode 100644 index 0000000..679d5d0 --- /dev/null +++ b/content/skin/sync16_1.png diff --git a/content/skin/sync16_2.png b/content/skin/sync16_2.png Binary files differnew file mode 100644 index 0000000..49fe7c8 --- /dev/null +++ b/content/skin/sync16_2.png diff --git a/content/skin/sync16_3.png b/content/skin/sync16_3.png Binary files differnew file mode 100644 index 0000000..87e3ee7 --- /dev/null +++ b/content/skin/sync16_3.png diff --git a/content/skin/sync16_4.png b/content/skin/sync16_4.png Binary files differnew file mode 100644 index 0000000..94cd442 --- /dev/null +++ b/content/skin/sync16_4.png diff --git a/content/skin/tbsync.png b/content/skin/tbsync.png Binary files differnew file mode 100644 index 0000000..be77a1f --- /dev/null +++ b/content/skin/tbsync.png diff --git a/content/skin/tbsync64.png b/content/skin/tbsync64.png Binary files differnew file mode 100644 index 0000000..0877be0 --- /dev/null +++ b/content/skin/tbsync64.png diff --git a/content/skin/tick16.png b/content/skin/tick16.png Binary files differnew file mode 100644 index 0000000..db93138 --- /dev/null +++ b/content/skin/tick16.png diff --git a/content/skin/todo16.png b/content/skin/todo16.png Binary files differnew file mode 100644 index 0000000..0023b3a --- /dev/null +++ b/content/skin/todo16.png diff --git a/content/skin/todo16_share.png b/content/skin/todo16_share.png Binary files differnew file mode 100644 index 0000000..46849be --- /dev/null +++ b/content/skin/todo16_share.png diff --git a/content/skin/trash16.png b/content/skin/trash16.png Binary files differnew file mode 100644 index 0000000..b8c10d3 --- /dev/null +++ b/content/skin/trash16.png diff --git a/content/skin/update32.png b/content/skin/update32.png Binary files differnew file mode 100644 index 0000000..f9621e8 --- /dev/null +++ b/content/skin/update32.png diff --git a/content/skin/user48.png b/content/skin/user48.png Binary files differnew file mode 100644 index 0000000..51bf808 --- /dev/null +++ b/content/skin/user48.png diff --git a/content/skin/warning16.png b/content/skin/warning16.png Binary files differnew file mode 100644 index 0000000..3f04b67 --- /dev/null +++ b/content/skin/warning16.png 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]); + } + } + } + } + } + } +}; |