/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Bookmarks Sync. * * The Initial Developer of the Original Code is Mozilla. * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dan Mills * Richard Newman * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ const EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils", "Utils", "Svc", "Str"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/ext/Observers.js"); Cu.import("resource://services-sync/ext/Preferences.js"); Cu.import("resource://services-sync/ext/StringBundle.js"); Cu.import("resource://services-sync/log4moz.js"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); // Constants for makeSyncCallback, waitForSyncCallback const CB_READY = {}; const CB_COMPLETE = {}; const CB_FAIL = {}; const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR; /* * Utility functions */ let Utils = { /** * Execute an arbitrary number of asynchronous functions one after the * other, passing the callback arguments on to the next one. All functions * must take a callback function as their last argument. The 'this' object * will be whatever asyncChain's is. * * @usage this._chain = Utils.asyncChain; * this._chain(this.foo, this.bar, this.baz)(args, for, foo) * * This is equivalent to: * * let self = this; * self.foo(args, for, foo, function (bars, args) { * self.bar(bars, args, function (baz, params) { * self.baz(baz, params); * }); * }); */ asyncChain: function asyncChain() { let funcs = Array.slice(arguments); let thisObj = this; return function callback() { if (funcs.length) { let args = Array.slice(arguments).concat(callback); let f = funcs.shift(); f.apply(thisObj, args); } }; }, /** * Wrap a function to catch all exceptions and log them * * @usage MyObj._catch = Utils.catch; * MyObj.foo = function() { this._catch(func)(); } * * Optionally pass a function which will be called if an * exception occurs. */ catch: function Utils_catch(func, exceptionCallback) { let thisArg = this; return function WrappedCatch() { try { return func.call(thisArg); } catch(ex) { thisArg._log.debug("Exception: " + Utils.exceptionStr(ex)); if (exceptionCallback) { return exceptionCallback.call(thisArg, ex); } return null; } }; }, /** * Wrap a function to call lock before calling the function then unlock. * * @usage MyObj._lock = Utils.lock; * MyObj.foo = function() { this._lock(func)(); } */ lock: function lock(label, func) { let thisArg = this; return function WrappedLock() { if (!thisArg.lock()) { throw "Could not acquire lock. Label: \"" + label + "\"."; } try { return func.call(thisArg); } finally { thisArg.unlock(); } }; }, isLockException: function isLockException(ex) { return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0; }, /** * Wrap functions to notify when it starts and finishes executing or if it got * an error. The message is a combination of a provided prefix and local name * with the current state and the subject is the provided subject. * * @usage function MyObj() { this._notify = Utils.notify("prefix:"); } * MyObj.foo = function() { this._notify(name, subject, func)(); } */ notify: function Utils_notify(prefix) { return function NotifyMaker(name, subject, func) { let thisArg = this; let notify = function(state) { let mesg = prefix + name + ":" + state; thisArg._log.trace("Event: " + mesg); Observers.notify(mesg, subject); }; return function WrappedNotify() { try { notify("start"); let ret = func.call(thisArg); notify("finish"); return ret; } catch(ex) { notify("error"); throw ex; } }; }; }, runInTransaction: function(db, callback, thisObj) { let hasTransaction = false; try { db.beginTransaction(); hasTransaction = true; } catch(e) { /* om nom nom exceptions */ } try { return callback.call(thisObj); } finally { if (hasTransaction) { db.commitTransaction(); } } }, // Prototype for mozIStorageCallback, used in queryAsync below. // This allows us to define the handle* functions just once rather // than on every queryAsync invocation. _storageCallbackPrototype: { results: null, // These are set by queryAsync. names: null, syncCb: null, handleResult: function handleResult(results) { if (!this.names) { return; } if (!this.results) { this.results = []; } let row; while ((row = results.getNextRow()) != null) { let item = {}; for each (name in this.names) { item[name] = row.getResultByName(name); } this.results.push(item); } }, handleError: function handleError(error) { this.syncCb.throw(error); }, handleCompletion: function handleCompletion(reason) { // If we got an error, handleError will also have been called, so don't // call the callback! We never cancel statements, so we don't need to // address that quandary. if (reason == REASON_ERROR) return; // If we were called with column names but didn't find any results, // the calling code probably still expects an array as a return value. if (this.names && !this.results) { this.results = []; } this.syncCb(this.results); } }, queryAsync: function(query, names) { // Synchronously asyncExecute fetching all results by name let storageCallback = {names: names, syncCb: Utils.makeSyncCallback()}; storageCallback.__proto__ = Utils._storageCallbackPrototype; query.executeAsync(storageCallback); return Utils.waitForSyncCallback(storageCallback.syncCb); }, byteArrayToString: function byteArrayToString(bytes) { return [String.fromCharCode(byte) for each (byte in bytes)].join(""); }, /** * Generate a string of random bytes. */ generateRandomBytes: function generateRandomBytes(length) { let rng = Cc["@mozilla.org/security/random-generator;1"] .createInstance(Ci.nsIRandomGenerator); let bytes = rng.generateRandomBytes(length); return Utils.byteArrayToString(bytes); }, /** * Encode byte string as base64url (RFC 4648). */ encodeBase64url: function encodeBase64url(bytes) { return btoa(bytes).replace('+', '-', 'g').replace('/', '_', 'g'); }, /** * GUIDs are 9 random bytes encoded with base64url (RFC 4648). * That makes them 12 characters long with 72 bits of entropy. */ makeGUID: function makeGUID() { return Utils.encodeBase64url(Utils.generateRandomBytes(9)); }, _base64url_regex: /^[-abcdefghijklmnopqrstuvwxyz0123456789_]{12}$/i, checkGUID: function checkGUID(guid) { return !!guid && this._base64url_regex.test(guid); }, ensureOneOpen: let (windows = {}) function ensureOneOpen(window) { // Close the other window if it exists let url = window.location.href; let other = windows[url]; if (other != null) other.close(); // Save the new window for future closure windows[url] = window; // Actively clean up when the window is closed window.addEventListener("unload", function() windows[url] = null, false); }, // Returns a nsILocalFile representing a file relative to the current // user's profile directory. The argument should be a string with // unix-style slashes for directory names (these slashes are automatically // converted to platform-specific path separators). getProfileFile: function getProfileFile(path) { return FileUtils.getFile("ProfD", path.split("/"), true); }, /** * Add a simple getter/setter to an object that defers access of a property * to an inner property. * * @param obj * Object to add properties to defer in its prototype * @param defer * Property of obj to defer to * @param prop * Property name to defer (or an array of property names) */ deferGetSet: function Utils_deferGetSet(obj, defer, prop) { if (Utils.isArray(prop)) return prop.map(function(prop) Utils.deferGetSet(obj, defer, prop)); let prot = obj.prototype; // Create a getter if it doesn't exist yet if (!prot.__lookupGetter__(prop)) { prot.__defineGetter__(prop, function () { return this[defer][prop]; }); } // Create a setter if it doesn't exist yet if (!prot.__lookupSetter__(prop)) { prot.__defineSetter__(prop, function (val) { this[defer][prop] = val; }); } }, /** * Determine if some value is an array * * @param val * Value to check (can be null, undefined, etc.) * @return True if it's an array; false otherwise */ isArray: function Utils_isArray(val) val != null && typeof val == "object" && val.constructor.name == "Array", lazyStrings: function Weave_lazyStrings(name) { let bundle = "chrome://weave/locale/services/" + name + ".properties"; return function() new StringBundle(bundle); }, deepEquals: function eq(a, b) { // If they're triple equals, then it must be equals! if (a === b) return true; // If they weren't equal, they must be objects to be different if (typeof a != "object" || typeof b != "object") return false; // But null objects won't have properties to compare if (a === null || b === null) return false; // Make sure all of a's keys have a matching value in b for (let k in a) if (!eq(a[k], b[k])) return false; // Do the same for b's keys but skip those that we already checked for (let k in b) if (!(k in a) && !eq(a[k], b[k])) return false; return true; }, deepCopy: function Weave_deepCopy(thing, noSort) { if (typeof(thing) != "object" || thing == null) return thing; let ret; if (Utils.isArray(thing)) { ret = []; for (let i = 0; i < thing.length; i++) ret.push(Utils.deepCopy(thing[i], noSort)); } else { ret = {}; let props = [p for (p in thing)]; if (!noSort) props = props.sort(); props.forEach(function(k) ret[k] = Utils.deepCopy(thing[k], noSort)); } return ret; }, // Works on frames or exceptions, munges file:// URIs to shorten the paths // FIXME: filename munging is sort of hackish, might be confusing if // there are multiple extensions with similar filenames formatFrame: function Utils_formatFrame(frame) { let tmp = ""; let file = frame.filename || frame.fileName; if (file) tmp = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1"); if (frame.lineNumber) tmp += ":" + frame.lineNumber; if (frame.name) tmp = frame.name + "()@" + tmp; return tmp; }, exceptionStr: function Weave_exceptionStr(e) { let message = e.message ? e.message : e; return message + " " + Utils.stackTrace(e); }, stackTraceFromFrame: function Weave_stackTraceFromFrame(frame) { let output = []; while (frame) { let str = Utils.formatFrame(frame); if (str) output.push(str); frame = frame.caller; } return output.join(" < "); }, stackTrace: function Weave_stackTrace(e) { // Wrapped nsIException if (e.location) return "Stack trace: " + Utils.stackTraceFromFrame(e.location); // Standard JS exception if (e.stack) return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < "). replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1"); return "No traceback available"; }, // Generator and discriminator for HMAC exceptions. // Split these out in case we want to make them richer in future, and to // avoid inevitable confusion if the message changes. throwHMACMismatch: function throwHMACMismatch(shouldBe, is) { throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is; }, isHMACMismatch: function isHMACMismatch(ex) { const hmacFail = "Record SHA256 HMAC mismatch: "; return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0); }, /** * UTF8-encode a message and hash it with the given hasher. Returns a * string containing bytes. The hasher is reset if it's an HMAC hasher. */ digestUTF8: function digestUTF8(message, hasher) { let data = this._utf8Converter.convertToByteArray(message, {}); hasher.update(data, data.length); let result = hasher.finish(false); if (hasher instanceof Ci.nsICryptoHMAC) { hasher.reset(); } return result; }, /** * Treat the given message as a bytes string and hash it with the given * hasher. Returns a string containing bytes. The hasher is reset if it's * an HMAC hasher. */ digestBytes: function digestBytes(message, hasher) { // No UTF-8 encoding for you, sunshine. let bytes = [b.charCodeAt() for each (b in message)]; hasher.update(bytes, bytes.length); let result = hasher.finish(false); if (hasher instanceof Ci.nsICryptoHMAC) { hasher.reset(); } return result; }, bytesAsHex: function bytesAsHex(bytes) { let hex = ""; for (let i = 0; i < bytes.length; i++) { hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2); } return hex; }, _sha256: function _sha256(message) { let hasher = Cc["@mozilla.org/security/hash;1"]. createInstance(Ci.nsICryptoHash); hasher.init(hasher.SHA256); return Utils.digestUTF8(message, hasher); }, sha256: function sha256(message) { return Utils.bytesAsHex(Utils._sha256(message)); }, sha256Base64: function (message) { return btoa(Utils._sha256(message)); }, _sha1: function _sha1(message) { let hasher = Cc["@mozilla.org/security/hash;1"]. createInstance(Ci.nsICryptoHash); hasher.init(hasher.SHA1); return Utils.digestUTF8(message, hasher); }, sha1: function sha1(message) { return Utils.bytesAsHex(Utils._sha1(message)); }, sha1Base32: function sha1Base32(message) { return Utils.encodeBase32(Utils._sha1(message)); }, sha1Base64: function (message) { return btoa(Utils._sha1(message)); }, /** * Produce an HMAC key object from a key string. */ makeHMACKey: function makeHMACKey(str) { return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str); }, /** * Produce an HMAC hasher and initialize it with the given HMAC key. */ makeHMACHasher: function makeHMACHasher(type, key) { let hasher = Cc["@mozilla.org/security/hmac;1"] .createInstance(Ci.nsICryptoHMAC); hasher.init(type, key); return hasher; }, /** * Some HMAC convenience functions for tests and backwards compatibility: * * sha1HMACBytes: hashes byte string, returns bytes string * sha256HMAC: hashes UTF-8 encoded string, returns hex string * sha256HMACBytes: hashes byte string, returns bytes string */ sha1HMACBytes: function sha1HMACBytes(message, key) { let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, key); return Utils.digestBytes(message, h); }, sha256HMAC: function sha256HMAC(message, key) { let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key); return Utils.bytesAsHex(Utils.digestUTF8(message, h)); }, sha256HMACBytes: function sha256HMACBytes(message, key) { let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key); return Utils.digestBytes(message, h); }, /** * HMAC-based Key Derivation Step 2 according to RFC 5869. */ hkdfExpand: function hkdfExpand(prk, info, len) { const BLOCKSIZE = 256 / 8; let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, Utils.makeHMACKey(prk)); let T = ""; let Tn = ""; let iterations = Math.ceil(len/BLOCKSIZE); for (let i = 0; i < iterations; i++) { Tn = Utils.digestBytes(Tn + info + String.fromCharCode(i + 1), h); T += Tn; } return T.slice(0, len); }, byteArrayToString: function byteArrayToString(bytes) { return [String.fromCharCode(byte) for each (byte in bytes)].join(""); }, /** * PBKDF2 implementation in Javascript. * * The arguments to this function correspond to items in * PKCS #5, v2.0 pp. 9-10 * * P: the passphrase, an octet string: e.g., "secret phrase" * S: the salt, an octet string: e.g., "DNXPzPpiwn" * c: the number of iterations, a positive integer: e.g., 4096 * dkLen: the length in octets of the destination * key, a positive integer: e.g., 16 * * The output is an octet string of length dkLen, which you * can encode as you wish. */ pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen) { // We don't have a default in the algo itself, as NSS does. // Use the constant. if (!dkLen) dkLen = SYNC_KEY_DECODED_LENGTH; /* For HMAC-SHA-1 */ const HLEN = 20; function F(S, c, i, h) { function XOR(a, b, isA) { if (a.length != b.length) { return false; } let val = []; for (let i = 0; i < a.length; i++) { if (isA) { val[i] = a[i] ^ b[i]; } else { val[i] = a.charCodeAt(i) ^ b.charCodeAt(i); } } return val; } let ret; let U = []; /* Encode i into 4 octets: _INT */ let I = []; I[0] = String.fromCharCode((i >> 24) & 0xff); I[1] = String.fromCharCode((i >> 16) & 0xff); I[2] = String.fromCharCode((i >> 8) & 0xff); I[3] = String.fromCharCode(i & 0xff); U[0] = Utils.digestBytes(S + I.join(''), h); for (let j = 1; j < c; j++) { U[j] = Utils.digestBytes(U[j - 1], h); } ret = U[0]; for (j = 1; j < c; j++) { ret = Utils.byteArrayToString(XOR(ret, U[j])); } return ret; } let l = Math.ceil(dkLen / HLEN); let r = dkLen - ((l - 1) * HLEN); // Reuse the key and the hasher. Remaking them 4096 times is 'spensive. let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, Utils.makeHMACKey(P)); T = []; for (let i = 0; i < l;) { T[i] = F(S, c, ++i, h); } let ret = ''; for (i = 0; i < l-1;) { ret += T[i++]; } ret += T[l - 1].substr(0, r); return ret; }, /** * Base32 decode (RFC 4648) a string. */ decodeBase32: function decodeBase32(str) { const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let padChar = str.indexOf("="); let chars = (padChar == -1) ? str.length : padChar; let bytes = Math.floor(chars * 5 / 8); let blocks = Math.ceil(chars / 8); // Process a chunk of 5 bytes / 8 characters. // The processing of this is known in advance, // so avoid arithmetic! function processBlock(ret, cOffset, rOffset) { let c, val; // N.B., this relies on // undefined | foo == foo. function accumulate(val) { ret[rOffset] |= val; } function advance() { c = str[cOffset++]; if (!c || c == "" || c == "=") // Easier than range checking. throw "Done"; // Will be caught far away. val = key.indexOf(c); if (val == -1) throw "Unknown character in base32: " + c; } // Handle a left shift, restricted to bytes. function left(octet, shift) (octet << shift) & 0xff; advance(); accumulate(left(val, 3)); advance(); accumulate(val >> 2); ++rOffset; accumulate(left(val, 6)); advance(); accumulate(left(val, 1)); advance(); accumulate(val >> 4); ++rOffset; accumulate(left(val, 4)); advance(); accumulate(val >> 1); ++rOffset; accumulate(left(val, 7)); advance(); accumulate(left(val, 2)); advance(); accumulate(val >> 3); ++rOffset; accumulate(left(val, 5)); advance(); accumulate(val); ++rOffset; } // Our output. Define to be explicit (and maybe the compiler will be smart). let ret = new Array(bytes); let i = 0; let cOff = 0; let rOff = 0; for (; i < blocks; ++i) { try { processBlock(ret, cOff, rOff); } catch (ex) { // Handle the detection of padding. if (ex == "Done") break; throw ex; } cOff += 8; rOff += 5; } // Slice in case our shift overflowed to the right. return Utils.byteArrayToString(ret.slice(0, bytes)); }, /** * Base32 encode (RFC 4648) a string */ encodeBase32: function encodeBase32(bytes) { const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let quanta = Math.floor(bytes.length / 5); let leftover = bytes.length % 5; // Pad the last quantum with zeros so the length is a multiple of 5. if (leftover) { quanta += 1; for (let i = leftover; i < 5; i++) bytes += "\0"; } // Chop the string into quanta of 5 bytes (40 bits). Each quantum // is turned into 8 characters from the 32 character base. let ret = ""; for (let i = 0; i < bytes.length; i += 5) { let c = [byte.charCodeAt() for each (byte in bytes.slice(i, i + 5))]; ret += key[c[0] >> 3] + key[((c[0] << 2) & 0x1f) | (c[1] >> 6)] + key[(c[1] >> 1) & 0x1f] + key[((c[1] << 4) & 0x1f) | (c[2] >> 4)] + key[((c[2] << 1) & 0x1f) | (c[3] >> 7)] + key[(c[3] >> 2) & 0x1f] + key[((c[3] << 3) & 0x1f) | (c[4] >> 5)] + key[c[4] & 0x1f]; } switch (leftover) { case 1: return ret.slice(0, -6) + "======"; case 2: return ret.slice(0, -4) + "===="; case 3: return ret.slice(0, -3) + "==="; case 4: return ret.slice(0, -1) + "="; default: return ret; } }, /** * Turn RFC 4648 base32 into our own user-friendly version. * ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 * becomes * abcdefghijk8mn9pqrstuvwxyz234567 */ base32ToFriendly: function base32ToFriendly(input) { return input.toLowerCase() .replace("l", '8', "g") .replace("o", '9', "g"); }, base32FromFriendly: function base32FromFriendly(input) { return input.toUpperCase() .replace("8", 'L', "g") .replace("9", 'O', "g"); }, /** * Key manipulation. */ // Return an octet string in friendly base32 *with no trailing =*. encodeKeyBase32: function encodeKeyBase32(keyData) { return Utils.base32ToFriendly( Utils.encodeBase32(keyData)) .slice(0, SYNC_KEY_ENCODED_LENGTH); }, decodeKeyBase32: function decodeKeyBase32(encoded) { return Utils.decodeBase32( Utils.base32FromFriendly( Utils.normalizePassphrase(encoded))) .slice(0, SYNC_KEY_DECODED_LENGTH); }, base64Key: function base64Key(keyData) { return btoa(keyData); }, deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) { return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength); } else { // Fall back to JS implementation. // 4096 is hardcoded in WeaveCrypto, so do so here. return Utils.pbkdf2Generate(passphrase, atob(salt), 4096, keyLength); } }, /** * N.B., salt should be base64 encoded, even though we have to decode * it later! */ derivePresentableKeyFromPassphrase : function derivePresentableKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { let k = Utils.deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS); return Utils.encodeKeyBase32(k); }, /** * N.B., salt should be base64 encoded, even though we have to decode * it later! */ deriveEncodedKeyFromPassphrase : function deriveEncodedKeyFromPassphrase(passphrase, salt, keyLength, forceJS) { let k = Utils.deriveKeyFromPassphrase(passphrase, salt, keyLength, forceJS); return Utils.base64Key(k); }, /** * Take a base64-encoded 128-bit AES key, returning it as five groups of five * uppercase alphanumeric characters, separated by hyphens. * A.K.A. base64-to-base32 encoding. */ presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) { return Utils.encodeKeyBase32(atob(encodedKey)); }, makeURI: function Weave_makeURI(URIString) { if (!URIString) return null; try { return Services.io.newURI(URIString, null, null); } catch (e) { let log = Log4Moz.repository.getLogger("Service.Util"); log.debug("Could not create URI: " + Utils.exceptionStr(e)); return null; } }, makeURL: function Weave_makeURL(URIString) { let url = Utils.makeURI(URIString); url.QueryInterface(Ci.nsIURL); return url; }, /** * Load a json object from disk * * @param filePath * Json file path load from weave/[filePath].json * @param that * Object to use for logging and "this" for callback * @param callback * Function to process json object as its first parameter */ jsonLoad: function Utils_jsonLoad(filePath, that, callback) { filePath = "weave/" + filePath + ".json"; if (that._log) that._log.trace("Loading json from disk: " + filePath); let file = Utils.getProfileFile(filePath); if (!file.exists()) { callback.call(that); return; } let channel = NetUtil.newChannel(file); channel.contentType = "application/json"; NetUtil.asyncFetch(channel, function (is, result) { if (!Components.isSuccessCode(result)) { callback.call(that); return; } let string = NetUtil.readInputStreamToString(is, is.available()); is.close(); let json; try { json = JSON.parse(string); } catch (ex) { if (that._log) that._log.debug("Failed to load json: " + Utils.exceptionStr(ex)); } callback.call(that, json); }); }, /** * Save a json-able object to disk * * @param filePath * Json file path save to weave/[filePath].json * @param that * Object to use for logging and "this" for callback * @param obj * Function to provide json-able object to save. If this isn't a * function, it'll be used as the object to make a json string. * @param callback * Function called when the write has been performed. Optional. */ jsonSave: function Utils_jsonSave(filePath, that, obj, callback) { filePath = "weave/" + filePath + ".json"; if (that._log) that._log.trace("Saving json to disk: " + filePath); let file = Utils.getProfileFile(filePath); let json = typeof obj == "function" ? obj.call(that) : obj; let out = JSON.stringify(json); let fos = FileUtils.openSafeFileOutputStream(file); let is = this._utf8Converter.convertToInputStream(out); NetUtil.asyncCopy(is, fos, function (result) { if (typeof callback == "function") { callback.call(that); } }); }, /** * Return a timer that is scheduled to call the callback after waiting the * provided time or as soon as possible. The timer will be set as a property * of the provided object with the given timer name. */ delay: function delay(callback, wait, thisObj, name) { // Default to running right away wait = wait || 0; // Use a dummy object if one wasn't provided thisObj = thisObj || {}; // Delay an existing timer if it exists if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) { thisObj[name].delay = wait; return; } // Create a special timer that we can add extra properties let timer = {}; timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); // Provide an easy way to clear out the timer timer.clear = function() { thisObj[name] = null; timer.cancel(); }; // Initialize the timer with a smart callback timer.initWithCallback({ notify: function notify() { // Clear out the timer once it's been triggered timer.clear(); callback.call(thisObj, timer); } }, wait, timer.TYPE_ONE_SHOT); return thisObj[name] = timer; }, getIcon: function(iconUri, defaultIcon) { try { let iconURI = Utils.makeURI(iconUri); return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec; } catch(ex) {} // Just give the provided default icon or the system's default return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec; }, getErrorString: function Utils_getErrorString(error, args) { try { return Str.errors.get(error, args || null); } catch (e) {} // basically returns "Unknown Error" return Str.errors.get("error.reason.unknown"); }, encodeUTF8: function(str) { try { str = this._utf8Converter.ConvertFromUnicode(str); return str + this._utf8Converter.Finish(); } catch(ex) { return null; } }, decodeUTF8: function(str) { try { str = this._utf8Converter.ConvertToUnicode(str); return str + this._utf8Converter.Finish(); } catch(ex) { return null; } }, /** * Generate 26 characters. */ generatePassphrase: function generatePassphrase() { // Note that this is a different base32 alphabet to the one we use for // other tasks. It's lowercase, uses different letters, and needs to be // decoded with decodeKeyBase32, not just decodeBase32. return Utils.encodeKeyBase32(Utils.generateRandomBytes(16)); }, /** * The following are the methods supported for UI use: * * * isPassphrase: * determines whether a string is either a normalized or presentable * passphrase. * * hyphenatePassphrase: * present a normalized passphrase for display. This might actually * perform work beyond just hyphenation; sorry. * * hyphenatePartialPassphrase: * present a fragment of a normalized passphrase for display. * * normalizePassphrase: * take a presentable passphrase and reduce it to a normalized * representation for storage. normalizePassphrase can safely be called * on normalized input. * * normalizeAccount: * take user input for account/username, cleaning up appropriately. */ isPassphrase: function(s) { if (s) { return /^[abcdefghijkmnpqrstuvwxyz23456789]{26}$/.test(Utils.normalizePassphrase(s)); } return false; }, /** * Hyphenate a passphrase (26 characters) into groups. * abbbbccccddddeeeeffffggggh * => * a-bbbbc-cccdd-ddeee-effff-ggggh */ hyphenatePassphrase: function hyphenatePassphrase(passphrase) { // For now, these are the same. return Utils.hyphenatePartialPassphrase(passphrase, true); }, hyphenatePartialPassphrase: function hyphenatePartialPassphrase(passphrase, omitTrailingDash) { if (!passphrase) return null; // Get the raw data input. Just base32. let data = passphrase.toLowerCase().replace(/[^abcdefghijkmnpqrstuvwxyz23456789]/g, ""); // This is the neatest way to do this. if ((data.length == 1) && !omitTrailingDash) return data + "-"; // Hyphenate it. let y = data.substr(0,1); let z = data.substr(1).replace(/(.{1,5})/g, "-$1"); // Correct length? We're done. if ((z.length == 30) || omitTrailingDash) return y + z; // Add a trailing dash if appropriate. return (y + z.replace(/([^-]{5})$/, "$1-")).substr(0, SYNC_KEY_HYPHENATED_LENGTH); }, normalizePassphrase: function normalizePassphrase(pp) { // Short var name... have you seen the lines below?! // Allow leading and trailing whitespace. pp = pp.trim().toLowerCase(); // 20-char sync key. if (pp.length == 23 && [5, 11, 17].every(function(i) pp[i] == '-')) { return pp.slice(0, 5) + pp.slice(6, 11) + pp.slice(12, 17) + pp.slice(18, 23); } // "Modern" 26-char key. if (pp.length == 31 && [1, 7, 13, 19, 25].every(function(i) pp[i] == '-')) { return pp.slice(0, 1) + pp.slice(2, 7) + pp.slice(8, 13) + pp.slice(14, 19) + pp.slice(20, 25) + pp.slice(26, 31); } // Something else -- just return. return pp; }, normalizeAccount: function normalizeAccount(acc) { return acc.trim(); }, // WeaveCrypto returns bad base64 strings. Truncate excess padding // and decode. // See Bug 562431, comment 4. safeAtoB: function safeAtoB(b64) { let len = b64.length; let over = len % 4; return over ? atob(b64.substr(0, len - over)) : atob(b64); }, /** * Create an array like the first but without elements of the second */ arraySub: function arraySub(minuend, subtrahend) { return minuend.filter(function(i) subtrahend.indexOf(i) == -1); }, /** * Build the union of two arrays. */ arrayUnion: function arrayUnion(foo, bar) { return foo.concat(Utils.arraySub(bar, foo)); }, bind2: function Async_bind2(object, method) { return function innerBind() { return method.apply(object, arguments); }; }, mpLocked: function mpLocked() { let modules = Cc["@mozilla.org/security/pkcs11moduledb;1"] .getService(Ci.nsIPKCS11ModuleDB); let sdrSlot = modules.findSlotByName(""); let status = sdrSlot.status; let slots = Ci.nsIPKCS11Slot; if (status == slots.SLOT_READY || status == slots.SLOT_LOGGED_IN || status == slots.SLOT_UNINITIALIZED) return false; if (status == slots.SLOT_NOT_LOGGED_IN) return true; // something wacky happened, pretend MP is locked return true; }, // If Master Password is enabled and locked, present a dialog to unlock it. // Return whether the system is unlocked. ensureMPUnlocked: function ensureMPUnlocked() { let sdr = Cc["@mozilla.org/security/sdr;1"] .getService(Ci.nsISecretDecoderRing); try { sdr.encryptString("bacon"); return true; } catch(e) {} return false; }, /** * Helpers for making asynchronous calls within a synchronous API possible. * * If you value your sanity, do not look closely at the following functions. */ /** * Check if the app is ready (not quitting) */ checkAppReady: function checkAppReady() { // Watch for app-quit notification to stop any sync calls Svc.Obs.add("quit-application", function() { Utils.checkAppReady = function() { throw Components.Exception("App. Quitting", Cr.NS_ERROR_ABORT); }; }); // In the common case, checkAppReady just returns true return (Utils.checkAppReady = function() true)(); }, /** * Create a sync callback that remembers state like whether it's been called */ makeSyncCallback: function makeSyncCallback() { // The main callback remembers the value it's passed and that it got data let onComplete = function onComplete(data) { onComplete.state = CB_COMPLETE; onComplete.value = data; }; // Initialize private callback data to prepare to be called onComplete.state = CB_READY; onComplete.value = null; // Allow an alternate callback to trigger an exception to be thrown onComplete.throw = function onComplete_throw(data) { onComplete.state = CB_FAIL; onComplete.value = data; // Cause the caller to get an exception and stop execution throw data; }; return onComplete; }, /** * Wait for a sync callback to finish */ waitForSyncCallback: function waitForSyncCallback(callback) { // Grab the current thread so we can make it give up priority let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread; // Keep waiting until our callback is triggered unless the app is quitting while (Utils.checkAppReady() && callback.state == CB_READY) { thread.processNextEvent(true); } // Reset the state of the callback to prepare for another call let state = callback.state; callback.state = CB_READY; // Throw the value the callback decided to fail with if (state == CB_FAIL) { throw callback.value; } // Return the value passed to the callback return callback.value; } }; XPCOMUtils.defineLazyGetter(Utils, "_utf8Converter", function() { let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; return converter; }); /* * Commonly-used services */ let Svc = {}; Svc.Prefs = new Preferences(PREFS_BRANCH); Svc.DefaultPrefs = new Preferences({branch: PREFS_BRANCH, defaultBranch: true}); Svc.Obs = Observers; let _sessionCID = Services.appinfo.ID == SEAMONKEY_ID ? "@mozilla.org/suite/sessionstore;1" : "@mozilla.org/browser/sessionstore;1"; [["Form", "@mozilla.org/satchel/form-history;1", "nsIFormHistory2"], ["Idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"], ["KeyFactory", "@mozilla.org/security/keyobjectfactory;1", "nsIKeyObjectFactory"], ["Session", _sessionCID, "nsISessionStore"] ].forEach(function([name, contract, iface]) { XPCOMUtils.defineLazyServiceGetter(Svc, name, contract, iface); }); // nsIPrivateBrowsingService is not implemented in mobile Firefox. // Svc.Private should just return undefined in this case instead of throwing. XPCOMUtils.defineLazyGetter(Svc, "Private", function() { try { return Cc["@mozilla.org/privatebrowsing;1"].getService(Ci["nsIPrivateBrowsingService"]); } catch (e) { return undefined; } }); Svc.__defineGetter__("Crypto", function() { let cryptoSvc; let ns = {}; Cu.import("resource://services-crypto/WeaveCrypto.js", ns); cryptoSvc = new ns.WeaveCrypto(); delete Svc.Crypto; return Svc.Crypto = cryptoSvc; }); let Str = {}; ["errors", "sync"].forEach(function(lazy) { XPCOMUtils.defineLazyGetter(Str, lazy, Utils.lazyStrings(lazy)); }); Svc.Obs.add("xpcom-shutdown", function () { for (let name in Svc) delete Svc[name]; });