/* Bundle Includes: * js/megaPromise.js * js/mDB.js * js/mouse.js * js/datastructs.js * js/idbkvstorage.js * js/sharedlocalkvstorage.js * js/tlvstore.js * js/vendor/jsbn.js * js/vendor/jsbn2.js * js/vendor/nacl-fast.js * js/authring.js * html/js/login.js * js/ui/export.js * html/js/key.js * js/ui/simpletip.js * js/useravatar.js * js/cms.js */ /** * Mega Promise * * Polyfill + easier to debug variant of Promises which are currently implemented in some of the cutting edge browsers. * * The main goals of using this, instead of directly using native Promises are: * - stack traces * - .done, .fail * - all js exceptions will be logged (in the console) and thrown as expected * * Note: for now, we will use $.Deferred to get this functionality out of the box and MegaPromise will act as a bridge * between the original Promise API and jQuery's Deferred APIs. * * Implementation note: .progress is currently not implemented. */ /** * Mega Promise constructor * * @returns {MegaPromise} * @constructor */ function MegaPromise(fn) { var self = this; this.$deferred = new $.Deferred(); this.state$deferred = this.$deferred; if (fn) { var resolve = function() { self.resolve.apply(self, arguments); }; var reject = function() { self.reject.apply(self, arguments); }; try { fn(resolve, reject); } catch (ex) { reject(ex); } } if (MegaPromise.debugPendingPromisesTimeout > 0) { var preStack = M.getStack(); setTimeout(function() { if (self.state() === 'pending') { console.error("Pending promise found: ", self, preStack); } }, MegaPromise.debugPendingPromisesTimeout); } if (MegaPromise.debugPreStack === true) { self.stack = M.getStack(); } } /** * Set this to any number (millisecond) and a timer would check if all promises are resolved in that time. If they are * still in 'pending' state, they will trigger an error (this is a debugging helper, not something that you should * leave on in production code!) * * @type {boolean|Number} */ MegaPromise.debugPendingPromisesTimeout = false; /** * Set this to true, to enable all promises to store a pre-stack in .stack. * * @type {boolean} */ MegaPromise.debugPreStack = false; /** * Convert Native and jQuery promises to MegaPromises, by creating a MegaPromise proxy which will be attached * to the actual underlying promise's .then callbacks. * * @param p * @returns {MegaPromise} * @private */ MegaPromise.asMegaPromiseProxy = function(p) { var $promise = new MegaPromise(); p.then( function megaPromiseResProxy() { $promise.resolve.apply($promise, arguments); }, MegaPromise.getTraceableReject($promise, p)); return $promise; }; /** * Show the loading dialog if a promise takes longer than 200ms * @returns {MegaPromise} */ MegaPromise.busy = function() { var promise = new MegaPromise(); if (fminitialized && !loadingDialog.active) { var timer = setTimeout(function() { timer = null; loadingDialog.pshow(); }, 200); promise.always(function() { if (timer) { clearTimeout(timer); } else { loadingDialog.phide(); } }); } return promise; }; /** * Common function to be used as reject callback to promises. * * @param promise {MegaPromise} * @returns {function} * @private */ MegaPromise.getTraceableReject = function($promise, origPromise) { 'use strict'; // Save the current stack pointer in case of an async call behind // the promise.reject (Ie, onAPIProcXHRLoad shown as initial call) var preStack = d > 1 && M.getStack(); return function __mpTraceableReject(aResult) { if (window.d > 1) { var postStack = M.getStack(); if (typeof console.group === 'function') { console.group('PROMISE REJECTED'); } console.debug('Promise rejected: ', aResult, origPromise); console.debug('pre-Stack', preStack); console.debug('post-Stack', postStack); if (typeof console.groupEnd === 'function') { console.groupEnd(); } } try { if (typeof $promise === 'function') { $promise.apply(origPromise, arguments); } else { $promise.reject.apply($promise, arguments); } } catch(e) { console.error('Unexpected promise error: ', e, preStack); } }; }; MegaPromise.prototype.benchmark = function(uniqueDebuggingName) { var self = this; MegaPromise._benchmarkTimes = MegaPromise._benchmarkTimes || {}; MegaPromise._benchmarkTimes[uniqueDebuggingName] = Date.now(); self.always(function() { console.error( uniqueDebuggingName, 'finished in:', Date.now() - MegaPromise._benchmarkTimes[uniqueDebuggingName] ); delete MegaPromise._benchmarkTimes[uniqueDebuggingName]; }); // allow chaining. return self; }; /** * By implementing this method, MegaPromise will be compatible with .when/.all syntax. * * jQuery: https://github.com/jquery/jquery/blob/10399ddcf8a239acc27bdec9231b996b178224d3/src/deferred.js#L133 * * @returns {jQuery.Deferred} */ MegaPromise.prototype.promise = function() { return this.$deferred.promise(); }; /** * Alias of .then * * @param res * Function to be called on resolution of the promise. * @param [rej] * Function to be called on rejection of the promise. * @returns {MegaPromise} */ MegaPromise.prototype.then = function(res, rej) { return MegaPromise.asMegaPromiseProxy(this.$deferred.then(res, rej)); }; /** * Alias of .done * * @param res * @returns {MegaPromise} */ MegaPromise.prototype.done = function(res) { this.$deferred.done(res); return this; }; /** * Alias of .state * * @returns {String} */ MegaPromise.prototype.state = function() { return this.$deferred.state(); }; /** * Alias of .fail * * @param rej * @returns {MegaPromise} */ MegaPromise.prototype.fail = function(rej) { this.$deferred.fail(rej); return this; }; /** * Alias of .fail * * @param rej * @returns {MegaPromise} */ MegaPromise.prototype.catch = MegaPromise.prototype.fail; /** * Alias of .resolve * * @returns {MegaPromise} */ MegaPromise.prototype.resolve = function() { this.state$deferred.resolve.apply(this.state$deferred, arguments); return this; }; /** * Alias of .reject * * @returns {MegaPromise} */ MegaPromise.prototype.reject = function() { this.state$deferred.reject.apply(this.state$deferred, arguments); return this; }; /** * Alias of .always * * @returns {MegaPromise} */ MegaPromise.prototype.always = function() { this.$deferred.always.apply(this.$deferred, arguments); return this; }; /** * Alias of .then, which works like .always and exchanges the internal Deferred promise. * * @returns {MegaPromise} */ MegaPromise.prototype.pipe = function(resolve, reject) { var pipe = this.then(resolve, reject || resolve); this.$deferred = pipe.$deferred; return pipe; }; /** * Alias of .always * * @returns {MegaPromise} */ MegaPromise.prototype.wait = function(callback) { // callback = tryCatch(callback); this.$deferred.always(function() { var args = toArray.apply(null, arguments); onIdle(function() { callback.apply(null, args); }); }); return this; }; /** * Alias of .wait * * @returns {MegaPromise} */ MegaPromise.prototype.finally = MegaPromise.prototype.wait; /** * Invoke promise fulfilment through try/catch and reject it if there's some exception... * @param {Function} resolve The function to invoke on fulfilment * @param {Function} [reject] The function to invoke on rejection/caught exceptions * @returns {MegaPromise} */ MegaPromise.prototype.tryCatch = function(resolve, reject) { 'use strict'; reject = reject || function() {}; return this.done(tryCatch(resolve, reject)).fail(reject); }; /** * Alias of .always * * @returns {MegaPromise} */ MegaPromise.prototype.unpack = function(callback) { // callback = tryCatch(callback); this.$deferred.always(function(result) { if (result.__unpack$$$) { // flatten an n-dimensional array. for (var i = result.length; i--;) { // pick the first argument for each member result[i] = result[i][0]; } result = Array.prototype.concat.apply([], result); } callback(result); }); return this; }; /** * Link the `targetPromise`'s state to the current promise. E.g. when targetPromise get resolved, the current promise * will get resolved too with the same arguments passed to targetPromise. * * PS: This is a simple DSL-like helper to save us from duplicating code when using promises :) * * @param targetPromise * @returns {MegaPromise} current promise, helpful for js call chaining */ MegaPromise.prototype.linkDoneTo = function(targetPromise) { var self = this; if (targetPromise instanceof MegaPromise) { // Using MegaPromise.done since it's more lightweight than the thenable // which creates a new deferred instance proxied back to MegaPromise... targetPromise.done(function() { self.resolve.apply(self, arguments); }); } else { targetPromise.then(function() { self.resolve.apply(self, arguments); }); } return this; }; /** * Link the `targetPromise`'s state to the current promise. E.g. when targetPromise get rejected, the current promise * will get rejected too with the same arguments passed to targetPromise. * PS: This is a simple DSL-like helper to save us from duplicating code when using promises :) * * * @param targetPromise * @returns {MegaPromise} current promise, helpful for js call chaining */ MegaPromise.prototype.linkFailTo = function(targetPromise) { var self = this; if (targetPromise instanceof MegaPromise) { // Using MegaPromise.fail since it's more lightweight than the thenable // which creates a new deferred instance proxied back to MegaPromise... targetPromise.fail(function() { self.reject.apply(self, arguments); }); } else { targetPromise.then(undefined, function() { self.reject.apply(self, arguments); }); } return this; }; /** * Link the `targetPromise`'s state to the current promise (both done and fail, see .linkDoneTo and .linkFailTo) * * PS: This is a simple DSL-like helper to save us from duplicating code when using promises :) * * @param targetPromise * @returns {MegaPromise} current promise, helpful for js call chaining */ MegaPromise.prototype.linkDoneAndFailTo = function(targetPromise) { this.linkDoneTo(targetPromise); this.linkFailTo(targetPromise); return this; }; /** * Link promise's state to a function's value. E.g. if the function returns a promise that promise's state will be * linked to the current fn. If it returns a non-promise-like value it will resolve/reject the current promise's value. * * PS: This is a simple DSL-like helper to save us from duplicating code when using promises :) * * @returns {MegaPromise} current promise, helpful for js call chaining */ MegaPromise.prototype.linkDoneAndFailToResult = function(cb, context, args) { var self = this; var ret = cb.apply(context, args); if (ret instanceof MegaPromise) { self.linkDoneTo(ret); self.linkFailTo(ret); } else { self.resolve(ret); } return self; }; /** * Development helper, that will dump the result/state change of this promise to the console * * @param [msg] {String} optional msg * @returns {MegaPromise} current promise, helpful for js call chaining */ MegaPromise.prototype.dumpToConsole = function(msg) { var self = this; if (d) { self.then( function () { console.log("success: ", msg ? msg : arguments, !msg ? null : arguments); }, function () { console.error("error: ", msg ? msg : arguments, !msg ? null : arguments); } ); } return self; }; MegaPromise.prototype.dump = MegaPromise.prototype.dumpToConsole; /** * Check if what we have is *potentially* another Promise implementation (Native, Bluebird, Q, etc) * @param {*|Object} p What we expect to be a promise. * @returns {Boolean} whether it is */ MegaPromise.isAnotherPromise = function(p) { 'use strict'; return !(p instanceof MegaPromise) && typeof Object(p).then === 'function'; }; /** * Implementation of Promise.all/$.when, with a little bit more flexible way of handling different type of promises * passed in the `promisesList` * * @returns {MegaPromise} */ MegaPromise.all = function(promisesList) { 'use strict'; var _jQueryPromisesList = promisesList.map(function(p) { if (MegaPromise.isAnotherPromise(p)) { p = MegaPromise.asMegaPromiseProxy(p); } if (d) { console.assert(p instanceof MegaPromise); } return p; }); var promise = new MegaPromise(); $.when.apply($, _jQueryPromisesList) .done(function megaPromiseResProxy() { promise.resolve(toArray.apply(null, arguments)); }) .fail(MegaPromise.getTraceableReject(promise)); return promise; }; /** * Implementation of Promise.all/$.when, with a little bit more flexible way of handling different type of promises * passed in the `promisesList`. * * Warning: This method will return a "master promise" which will only get resolved when ALL promises had finished * processing (e.g. changed their state to either resolved or rejected). The only case when the master promise will get, * rejected is if there are still 'pending' promises in the `promisesList` after the `timeout` * * @param promisesList {Array} * @param [timeout] {Integer} max ms to way for the master promise to be resolved before rejecting it * @returns {MegaPromise} */ MegaPromise.allDone = function(promisesList, timeout) { // IF empty, resolve immediately if (promisesList.length === 0) { return MegaPromise.resolve(); } var masterPromise = new MegaPromise(); var totalLeft = promisesList.length; var results = []; results.__unpack$$$ = 1; var alwaysCb = function() { results.push(toArray.apply(null, arguments)); if (--totalLeft === 0) { masterPromise.resolve(results); } }; for (var i = promisesList.length; i--;) { var v = promisesList[i]; if (MegaPromise.isAnotherPromise(v)) { v = MegaPromise.asMegaPromiseProxy(v); } if (v instanceof MegaPromise) { v.done(alwaysCb); v.fail(alwaysCb); } else { if (d) { console.warn('non-promise provided...', v); } alwaysCb(v); } } if (timeout) { var timeoutTimer = setTimeout(function () { masterPromise.reject(results); }, timeout); masterPromise.always(function () { clearTimeout(timeoutTimer); }); } return masterPromise; }; /** * alias of Promise.resolve, will create a new promise, resolved with the arguments passed to this method * * @returns {MegaPromise} */ MegaPromise.resolve = function() { var p = new MegaPromise(); p.resolve.apply(p, arguments); return p; }; /** * alias of Promise.reject, will create a new promise, rejected with the arguments passed to this method * * @returns {MegaPromise} */ MegaPromise.reject = function() { var p = new MegaPromise(); p.reject.apply(p, arguments); return p; }; /** * Development helper tool to delay .resolve/.reject of a promise. * * @param ms {Number} milliseconds to delay the .resolve/.reject */ MegaPromise.prototype.fakeDelay = function(ms) { var self = this; if (self._fakeDelayEnabled) { return; } var origResolve = self.resolve; var origReject = self.reject; self.resolve = function() { var args = arguments; setTimeout(function() { origResolve.apply(self, args); }, ms); return self; }; self.reject = function() { var args = arguments; setTimeout(function() { origReject.apply(self, args); }, ms); return self; }; self._fakeDelayEnabled = true; return self; }; /** * Helper tool, that creates a new queue, that can be used for scheduling callbacks, which return promises. * So, every callback would ONLY be executed AFTER the previously queued one finishes execution. * * @constructor */ MegaPromise.QueuedPromiseCallbacks = function() { /*jshint -W057 */ return new (function(queueName, debug) { /*jshint +W057 */ var self = this; if (!queueName) { queueName = "untitledQueue" + parseInt(rand_range(0, 1000)); } self._queued = []; self.queue = function(fn, fnName) { var masterPromise = new MegaPromise(); self._queued.push({ 'cb': fn, 'name': fnName, 'masterPromise': masterPromise }); if (debug) { console.log("-=> Added to queue", queueName, " FN#", fnName ? fnName : self._queued.length); } return masterPromise; }; self.tick = function() { if ( self.currentQueuedEntry && self.currentQueuedEntry.masterPromise.state() === 'pending' ) { return; } if (self._queued.length === 0) { // all done! if (self._finishedCb) { self._finishedCb(); } return; } var currentQueuedEntry = self._queued.shift(); var currentName = currentQueuedEntry && currentQueuedEntry.name ? currentQueuedEntry.name : "noname"; // set immediately, used as an implicit lock. self.currentQueuedEntry = currentQueuedEntry; if (debug) { console.log( "-=> PromiseQueue", queueName, "Starting FN#", currentName ); } var execPromise = currentQueuedEntry.resultPromise = currentQueuedEntry.cb(); currentQueuedEntry.masterPromise.linkDoneAndFailTo(execPromise); currentQueuedEntry.executionTimeoutPromise = createTimeoutPromise( function() { return execPromise.state() !== 'pending'; }, 500, 10000 ) .fail(function() { if (typeof console !== 'undefined' && typeof console.warn !== 'undefined') { // this is an important message, that we want to be shown on production. console.warn('promise in promiseQueue ' + queueName + ' had timed out!'); } }) .done(function() { if (debug) { console.log("-=> PromiseQueue", queueName, "Finished FN#", currentName); } self.tick(); }); execPromise.always(function() { currentQueuedEntry.executionTimeoutPromise.verify(); }); }; self.whenFinished = function(cb) { self._finishedCb = cb; return self; }; })(); }; // FM IndexedDB layer (using Dexie.js - https://github.com/dfahlander/Dexie.js) // (indexes and payload are obfuscated using AES ECB - FIXME: use CBC for the payload) // DB name is fm_ + encrypted u_handle (folder links are not cached yet - FIXME) // init() checks for the presence of a valid _sn record and wipes the DB if none is found // pending[] is an array of write transactions that will be streamed to the DB // setting pending[]._sn opens a new transaction, so always set it last // - small updates run as a physical IndexedDB transaction // - large updates are written on the fly, but with the _sn cleared, which // ensures integrity, but invalidates the DB if the update can't complete // plainname: the base name that will be obfuscated using u_k // schema: the Dexie database schema // channelmap: { tablename : channel } - tables that do not map to channel 0 // (only channel 0 operates under an _sn-triggered transaction regime) function FMDB(plainname, schema, channelmap) { 'use strict'; if (!(this instanceof FMDB)) { return new FMDB(plainname, schema, channelmap); } // DB name suffix, derived from u_handle and u_k this.name = false; // DB schema - https://github.com/dfahlander/Dexie.js/wiki/TableSchema this.schema = schema; // the table names contained in the schema (set at open) this.tables = null; // if we have non-transactional (write-through) tables, they are mapped // to channel numbers > 0 here this.channelmap = channelmap || {}; // pending obfuscated writes [channel][tid][tablename][action_autoincrement] = [payloads] this.pending = [Object.create(null)]; // current channel tid being written to (via .add()/.del()) by the application code this.head = [0]; // current channel tid being sent to IndexedDB this.tail = [0]; // -1: idle, 0: deleted sn and writing (or write-through), 1: transaction open and writing this.state = -1; // flag indicating whether there is a pending write this.writing = false; // [tid, tablename, action] of .pending[] hash item currently being written this.inflight = false; // the write is complete and needs be be committed (either because of _sn or write-through) this.commit = false; // a DB error occurred, do not touch IndexedDB for the rest of the session this.crashed = false; // DB invalidation process: callback and ready flag this.inval_cb = false; this.inval_ready = false; // whether multi-table transactions work (1) or not (0) (Apple, looking at you!) this.cantransact = -1; // a flag to know if we have sn set in database. -1 = we don't know, 0 = not set, 1 = is set this.sn_Set = -1; // @see {@link FMDB.compare} this._cache = Object.create(null); // initialise additional channels for (var i in this.channelmap) { i = this.channelmap[i]; this.head[i] = 0; this.tail[i] = 0; this.pending[i] = Object.create(null); } // protect user identity post-logout this.name = ab_to_base64(this.strcrypt((plainname + plainname).substr(0, 16))); // console logging this.logger = MegaLogger.getLogger('FMDB'); this.logger.options.printDate = false; this.logger.options.levelColors = { 'ERROR': '#fe000b', 'DEBUG': '#005aff', 'WARN': '#d66d00', 'INFO': '#2ca100', 'LOG': '#5b5352' }; // if (d) Dexie.debug = "dexie"; } tryCatch(function() { 'use strict'; // Check for indexedDB 2.0 + binary keys support. Object.defineProperty(FMDB, 'iDBv2', {value: indexedDB.cmp(new Uint8Array(0), 0)}); }, false)(); // options FMDB.$useBinaryKeys = FMDB.iDBv2 ? 1 : 0; FMDB.$usePostSerialz = 2; // @private increase to drop/recreate *all* databases. FMDB.version = 1; // @private Object.defineProperty(FMDB, 'capabilities', { value: FMDB.iDBv2 << 4 | (FMDB.$useBinaryKeys | FMDB.$usePostSerialz /* | ... */) }); // @private persistence prefix Object.defineProperty(FMDB, 'perspex', {value: '.' + FMDB.version + FMDB.capabilities.toString(32)}); // initialise cross-tab access arbitration identity FMDB.prototype.identity = Date.now() + Math.random().toString(26); // set up and check fm DB for user u // calls result(sn) if found and sn present // wipes DB an calls result(false) otherwise FMDB.prototype.init = function fmdb_init(result, wipe) { "use strict"; var fmdb = this; var dbpfx = 'fm30_'; var slave = !mBroadcaster.crossTab.master; fmdb.crashed = false; fmdb.inval_cb = false; fmdb.inval_ready = false; // prefix database name with options/capabilities dbpfx += FMDB.perspex.substr(1); // Make the database name dependent on the current schema. dbpfx += MurmurHash3(JSON.stringify(this.schema), 0x6f01f).toString(16); // Notify completion invoking the provided callback var resolve = function(sn, error) { fmdb.opening = false; if (typeof result === 'function') { if (error) { fmdb.crashed = 2; fmdb.logger.warn('Marking DB as crashed.', error); if (fmdb.db) { onIdle(function() { fmdb.db.delete(); fmdb.db = false; }); } eventlog(99724, '$init:' + error, true); } result(sn); // prevent this from being called twice.. result = null; } }; // Catch errors, mark DB as crashed, and move forward without indexedDB support var reject = function(e) { resolve(false, e || EFAILED); }; // Database opening logic var openDataBase = function() { // start inter-tab heartbeat // fmdb.beacon(); fmdb.db = new Dexie(dbpfx + fmdb.name); // There is some inconsistency in Chrome 58.0.3029.110 that could cause indexedDB OPs to take ages... setTimeout(function() { // if not resolved already... if (result !== null) { if (d) { fmdb.logger.warn('Opening the database timed out.'); } reject(ETEMPUNAVAIL); } }, 15000); var dbSchema = {}; if (!Array.isArray(fmdb.schema)) { fmdb.schema = [fmdb.schema]; } for (var i = 0; i < fmdb.schema.length; i++) { var schema = fmdb.schema[i]; for (var k in schema) { if (schema.hasOwnProperty(k)) { dbSchema[k] = schema[k]; } } fmdb.db.version(i + 1).stores(dbSchema); } fmdb.tables = Object.keys(dbSchema); fmdb.db.open().then(function() { if (fmdb.crashed) { // Opening timed out. return; } fmdb.get('_sn').always(function(r) { if (!wipe && r[0] && r[0].length === 11) { if (d) { fmdb.logger.log("DB sn: " + r[0]); } resolve(r[0]); } else if (slave || fmdb.crashed) { fmdb.crashed = 2; resolve(false); } else { if (d) { fmdb.logger.log("No sn found in DB, wiping..."); } fmdb.db.delete().then(function() { fmdb.db.open().then(function() { resolve(false); }).catch(reject); }).catch(reject); } }); }).catch(Dexie.MissingAPIError, function(e) { fmdb.logger.error("IndexedDB unavailable", e); reject(e); }).catch(reject); }; openDataBase = tryCatch(openDataBase, reject); // Enumerate databases and collect those not prefixed with 'dbpfx' (which is the current format) var collectDataBaseNames = function() { var timer; var todrop = []; var done = function() { clearTimeout(timer); fmdb.dropall(todrop, openDataBase); done = null; }; if (d) { fmdb.logger.log('Collecting database names...'); } Dexie.getDatabaseNames(function(r) { if (sessionStorage.fmdbDropALL) { todrop = r; fmdb.logger.warn('drop all...', r); return; } for (var i = r.length; i--;) { // drop only fmX related databases and skip slkv's if (r[i][0] !== '$' && r[i].substr(0, dbpfx.length) !== dbpfx && r[i].substr(-FMDB.perspex.length) !== FMDB.perspex) { todrop.push(r[i]); } } }).finally(function() { if (d) { if (todrop.length) { fmdb.logger.log("Deleting obsolete DBs: " + todrop.join(', ')); } else { fmdb.logger.log('No databases collected...'); } } if (done) { done(); } }); timer = setTimeout(function() { if (d) { fmdb.logger.warn('Dexie.getDatabaseNames timed out...'); } done(); }, 3000); }; collectDataBaseNames = tryCatch(collectDataBaseNames, openDataBase); // Let's start the fun... if (!fmdb.up()) { resolve(false); } else if (!fmdb.db) { if (fmdb.opening) { fmdb.logger.error('Something went wrong... a DB is already opening...'); } else { // Collect obsolete databases to remove them, and proceed opening our current database collectDataBaseNames(); fmdb.opening = true; } } else { console.error('fmdb.db is already set...'); } }; // drop database FMDB.prototype.drop = function fmdb_drop() { var promise = new MegaPromise(); if (!this.db) { promise.resolve(); } else { var fmdb = this; this.invalidate(function() { fmdb.db.delete().then(function() { fmdb.logger.debug("IndexedDB deleted..."); }).catch(function(err) { fmdb.logger.error("Unable to delete IndexedDB!", err); }).finally(function() { promise.resolve(); }); this.db = null; }); } return promise; }; // drop random databases FMDB.prototype.dropall = function fmdb_dropall(dbs, cb) { if (!dbs || !dbs.length) { cb(); } else { var fmdb = this; var db = new Dexie(dbs.pop()); var next = function(ev) { next = function() {}; if (ev && ev.type === 'blocked') { fmdb.logger.warn('Cannot delete blocked indexedDB: ' + db.name); } fmdb.dropall(dbs, cb); }; // If the DB is blocked, Dexie will try to delete it as soon there are no locks on it. // However, we'll resolve immediately without waiting for it, since that will happen in // an undetermined amount of time which needless to say is an odd UX experience... db.on('blocked', next); db.delete().then(function() { fmdb.logger.log("Deleted IndexedDB " + db.name); }).catch(function(err){ fmdb.logger.error("Unable to delete IndexedDB " + db.name, err); }).finally(function() { next(); }); } }; // check if data for table is currently being written. FMDB.prototype.hasPendingWrites = function(table) { 'use strict'; if (!table) { return this.writing; } var ch = this.channelmap[table] || 0; var ps = this.pending[ch][this.tail[ch]] || false; return this.tail[ch] !== this.head[ch] && ps[table]; }; // enqueue a table write - type 0 == addition, type 1 == deletion // IndexedDB activity is triggered once we have a few thousand of pending rows or the sn // (writing the sn - which is done last - completes the transaction and starts a new one) FMDB.prototype.enqueue = function fmdb_enqueue(table, row, type) { "use strict"; var c; var fmdb = this; var ch = fmdb.channelmap[table] || 0; // if needed, create new transaction at index fmdb.head if (!(c = fmdb.pending[ch][fmdb.head[ch]])) { c = fmdb.pending[ch][fmdb.head[ch]] = Object.create(null); } // if needed, create new hash of modifications for this table // .h = head, .t = tail (last written to the DB) if (!c[table]) { // even indexes hold additions, odd indexes hold deletions c[table] = { t : -1, h : type }; c = c[table]; // @todo is deduplicating nodes in Safari still needed?.. if (table === 'f' && window.safari) { c.r = Object.create(null); } } else { // (we continue to use the highest index if it is of the requested type // unless it is currently in flight) // increment .h(head) if needed c = c[table]; if ((c.h ^ type) & 1) c.h++; } if (!c[c.h]) { c[c.h] = [row]; } else if (type || !c.r) { c[c.h].push(row); } else if (c.r[row.h]) { c[c.h][c.r[row.h]] = row; } else { c.r[row.h] = c[c.h].push(row) - 1; } // force a flush when a lot of data is pending or the _sn was updated // also, force a flush for non-transactional channels (> 0) if (ch || table[0] === '_' || c[c.h].length > 12281) { // the next write goes to a fresh transaction if (!ch) { fmdb.head[ch]++; } fmdb.writepending(fmdb.head.length - 1); } }; /** * Serialize data before storing it into indexedDB * @param {String} table The table this dta belongs to * @param {Object} row Object to serialize. * @returns {Object} The input data serialized */ FMDB.prototype.serialize = function(table, row) { 'use strict'; if (row.d) { if (this.stripnode[table]) { // this node type is stripnode-optimised: temporarily remove redundant elements // to create a leaner JSON and save IndexedDB space var j = row.d; // this references the live object! var t = this.stripnode[table](j); // remove overhead row.d = JSON.stringify(j); // store lean result // Restore overhead (In Firefox, Object.assign() is ~63% faster than for..in) Object.assign(j, t); } else { // otherwise, just stringify it all row.d = JSON.stringify(row.d); } } // obfuscate index elements as base64-encoded strings, payload as ArrayBuffer for (var i in row) { if (i === 'd') { row.d = this.strcrypt(row.d); } else if (table !== 'f' || i !== 't') { row[i] = this.toStore(row[i]); } } return row; }; // FIXME: auto-retry smaller transactions? (need stats about transaction failures) // ch - channel to operate on FMDB.prototype.writepending = function fmdb_writepending(ch) { "use strict"; // exit loop if we ran out of pending writes or have crashed if (this.inflight || ch < 0 || this.crashed || this.writing) { return; } // signal when we start/finish to save stuff if (!ch) { if (this.tail[ch] === this.head[ch] - 1) { document.documentElement.classList.add('fmdb-working'); } else if (this.tail[ch] === this.head[ch]) { document.documentElement.classList.remove('fmdb-working'); } } // iterate all channels to find pending writes if (!this.pending[ch][this.tail[ch]]) { return this.writepending(ch - 1); } if (this.tail[ch] >= this.head[ch]) { return; } var fmdb = this; if (d > 1) { fmdb.logger.warn('writepending()', ch, fmdb.state, Object(fmdb.pending[0][fmdb.tail[0]])._sn, fmdb.cantransact); } if (!ch && fmdb.state < 0 && fmdb.cantransact) { // if the write job is on channel 0 and already complete (has _sn set), // we execute it in a single transaction without first clearing sn fmdb.state = 1; fmdb.writing = 1; fmdb.db.transaction('rw', fmdb.tables, function() { if (d) { fmdb.logger.info("Transaction started"); console.time('fmdb-transaction'); } fmdb.commit = false; fmdb.cantransact = 1; if (fmdb.sn_Set && !fmdb.pending[0][fmdb.tail[0]]._sn && currsn) { fmdb.db._sn.clear().then(function() { fmdb.sn_Set = 0; dispatchputs(); }); } else { dispatchputs(); } }).then(function() { // transaction completed: delete written data delete fmdb.pending[0][fmdb.tail[0]++]; if (d) { fmdb.logger.log("HEAD = " + fmdb.head[0] + " --- Tail = " + fmdb.tail[0]); } fmdb.state = -1; if (d) { fmdb.logger.info("Transaction committed"); console.timeEnd('fmdb-transaction'); } fmdb.writing = 0; fmdb.writepending(ch); }).catch(function(ex) { if (d) { console.timeEnd('fmdb-transaction'); } if (fmdb.cantransact < 0) { fmdb.logger.error("Your browser's IndexedDB implementation is bogus, disabling transactions."); fmdb.cantransact = 0; fmdb.writing = 0; fmdb.writepending(ch); } else { // FIXME: retry instead? need statistics. fmdb.logger.error("Transaction failed, marking DB as crashed", ex); fmdb.state = -1; fmdb.invalidate(); eventlog(99724, '$wptr:' + ex, true); } }); } else { if (d) { console.error('channel 1 Block ... invoked'); } // we do not inject write-through operations into a live transaction if (fmdb.state > 0) { dispatchputs(); } else { // the job is incomplete or non-transactional - set state to "executing // write without transaction" fmdb.state = 0; if (ch) { // non-transactional channel: go ahead and write dispatchputs(); } else { // mark db as "writing" until the sn cleaning have completed, // this flag will be reset on dispatchputs() once fmdb.commit is set fmdb.writing = 2; // we clear the sn (the new sn will be written as the last action in this write job) // unfortunately, the DB will have to be wiped in case anything goes wrong var sendOperation = function() { fmdb.commit = false; fmdb.writing = 3; dispatchputs(); }; if (currsn) { fmdb.db._sn.clear().then( function() { if (d) { console.error('channel 1 + Sn cleared'); } fmdb.sn_Set = 0; sendOperation(); } ).catch(function(e) { fmdb.logger.error("SN clearing failed, marking DB as crashed", e); fmdb.state = -1; fmdb.invalidate(); eventlog(99724, '$wpsn:' + e, true); }); } else { sendOperation(); } } } } // start writing all pending data in this transaction to the DB // conclude/commit the (virtual or real) transaction once _sn has been written function dispatchputs() { if (fmdb.inflight) return; if (fmdb.commit) { // invalidation commit completed? if (fmdb.inval_ready) { if (fmdb.inval_cb) { // fmdb.db.close(); fmdb.inval_cb(); // caller must not reuse fmdb object } return; } // the transaction is complete: delete from pending if (!fmdb.state) { // we had been executing without transaction protection, delete the current // transaction and try to dispatch the next one immediately if (!ch) delete fmdb.pending[0][fmdb.tail[0]++]; fmdb.commit = false; fmdb.state = -1; fmdb.writing = false; fmdb.writepending(ch); } // if we had a real IndexedDB transaction open, it will commit // as soon as the browser main thread goes idle // I wont return, because this is relying on processing _sn table as the last // table in the current pending operations.. // return; } var tablesremaining = false; // this entirely relies on non-numeric hash keys being iterated // in the order they were added. FIXME: check if always true for (var table in fmdb.pending[ch][fmdb.tail[ch]]) { // iterate through pending tables, _sn last var t = fmdb.pending[ch][fmdb.tail[ch]][table]; // do we have at least one update pending? (could be multiple) if (t[t.h]) { tablesremaining = true; // locate next pending table update (even/odd: put/del) while (t.t <= t.h && !t[t.t]) t.t++; // all written: advance head if (t.t == t.h) t.h++; if (fmdb.crashed && !(t.t & 1)) { if (d) { fmdb.logger.warn('The DB is crashed, halting put...'); } return; } if (d) { fmdb.logger.debug("DB %s with %s element(s) on table %s, channel %s, state %s", (t.t & 1) ? 'del' : 'put', t[t.t].length, table, ch, fmdb.state); } // if we are on a non-transactional channel or the _sn is being updated, // request a commit after the operation completes. if (ch || table[0] == '_') { fmdb.commit = true; fmdb.sn_Set = 1; } // record what we are sending... fmdb.inflight = t; // is this an in-band _sn invalidation, and do we have a callback set? arm it. if (fmdb.inval_cb && t.t & 1 && table[0] === '_') { fmdb.inval_ready = true; } // ...and send update off to IndexedDB for writing write(table, t[t.t], t.t++ & 1 ? 'bulkDelete' : 'bulkPut'); // we don't send more than one transaction (looking at you, Microsoft!) return; } else { // if we are non-transactional and all data has been written for this // table, we can safely delete its record if (!fmdb.state && t.t == t.h) { delete fmdb.pending[ch][fmdb.tail[ch]][table]; } } } // if we are non-transactional, this deletes the "transaction" when done // (as commit will never be set) if (!fmdb.state && !tablesremaining) { delete fmdb.pending[ch][fmdb.tail[ch]]; fmdb.writing = null; fmdb.writepending(fmdb.head.length - 1); } } // bulk write operation function write(table, data, op) { var limit = window.fminitialized ? 1536 : data.length; if (FMDB.$usePostSerialz) { if (d) { console.time('fmdb-serialize'); } if (op === 'bulkPut') { for (var x = data.length; x--;) { fmdb.serialize(table, data[x]); } } else { for (var j = data.length; j--;) { data[j] = fmdb.toStore(data[j]); } } if (d) { console.timeEnd('fmdb-serialize'); } } if (data.length < limit) { fmdb.db[table][op](data).then(writeend).catch(writeerror); return; } var idx = 0; (function bulkTick() { var rows = data.slice(idx, idx += limit); if (rows.length) { if (d > 1) { var left = idx > data.length ? 0 : data.length - idx; fmdb.logger.log('%s for %d rows, %d remaining...', op, rows.length, left); } fmdb.db[table][op](rows).then(bulkTick).catch(writeerror); } else { data = undefined; writeend(); } })(); } // event handler for bulk operation completion function writeend() { if (d) { fmdb.logger.log('DB write successful' + (fmdb.commit ? ' - transaction complete' : '') + ', state: ' + fmdb.state); } // if we are non-transactional, remove the written data from pending // (we have to keep it for the transactional case because it needs to // be visible to the pending updates search that getbykey() performs) if (!fmdb.state) { delete fmdb.inflight[fmdb.inflight.t - 1]; fmdb.inflight = false; // in non-transactional loop back when the browser is idle so that we'll // prevent unnecessarily hanging the main thread and short writes... if (!fmdb.commit) { if (loadfm.loaded) { onIdle(dispatchputs); } else { setTimeout(dispatchputs, 2600); } return; } } // loop back to write more pending data (or to commit the transaction) fmdb.inflight = false; dispatchputs(); } // event handler for bulk operation error function writeerror(ex) { if (ex instanceof Dexie.BulkError) { fmdb.logger.error('Bulk operation error, %s records failed.', ex.failures.length, ex); } else { fmdb.logger.error('Unexpected error in bulk operation...', ex); } fmdb.state = -1; fmdb.inflight = false; // If there is an invalidation request pending, dispatch it. if (fmdb.inval_cb) { console.assert(fmdb.crashed, 'Invalid state, the DB must be crashed already...'); fmdb.inval_cb(); } else { fmdb.invalidate(); } if (d) { fmdb.logger.warn('Marked DB as crashed...', ex.name); } eventlog(99724, String(ex), true); } }; /** * Encrypt Unicode string with user's master key * @param {String} s The unicode string * @returns {ArrayBuffer} encrypted buffer * @todo use CBC instead of ECB! */ FMDB.prototype.strcrypt = function fmdb_strcrypt(s) { "use strict"; if (d && String(s).length > 0x10000) { (this.logger || console) .warn('The data you are trying to write is too large and will degrade the performance...', [s]); } var len = (s = '' + s).length; var bytes = this.utf8length(s); if (bytes === len) { var a32 = new Int32Array(len + 3 >> 2); for (var i = len; i--;) { a32[i >> 2] |= s.charCodeAt(i) << 8 * (i & 3); } return this._crypt(u_k_aes, a32); } return this._crypt(u_k_aes, this.to8(s, bytes)); }; /** * Decrypt buffer with user's master key * @param {ArrayBuffer} buffer Encrypted buffer * @returns {String} unicode string * @see {@link FMDB.strcrypt} */ FMDB.prototype.strdecrypt = function fmdb_strdecrypt(buffer) { "use strict"; if (buffer.byteLength) { var s = this.from8(this._decrypt(u_k_aes, buffer)); for (var i = s.length; i--;) { if (s.charCodeAt(i)) { return s.substr(0, i + 1); } } } return ''; }; // @private legacy version FMDB.prototype.strcrypt0 = function fmdb_strcrypt(s) { "use strict"; if (d && String(s).length > 0x10000) { console.warn('The data you are trying to write is too huge and will degrade the performance...'); } var a32 = str_to_a32(to8(s)); for (var i = (-a32.length) & 3; i--; ) a32.push(0); return a32_to_ab(encrypt_key(u_k_aes, a32)).buffer; }; // @private legacy version FMDB.prototype.strdecrypt0 = function fmdb_strdecrypt(ab) { "use strict"; if (!ab.byteLength) return ''; var a32 = []; var dv = new DataView(ab); for (var i = ab.byteLength/4; i--; ) a32[i] = dv.getUint32(i*4); var s = from8(a32_to_str(decrypt_key(u_k_aes, a32))); for (var i = s.length; i--; ) if (s.charCodeAt(i)) return s.substr(0, i+1); }; // TODO: @lp/@diego we need to move this to some other place... FMDB._mcfCache = {}; // remove fields that are duplicated in or can be inferred from the index to reduce database size FMDB.prototype.stripnode = Object.freeze({ f : function(f) { 'use strict'; var t = { h : f.h, t : f.t, s : f.s }; // Remove pollution from the ufs-size-cache // 1. non-folder nodes does not need tb/td/tf if (!f.t) { delete f.tb; delete f.td; delete f.tf; } // 2. remove Zero properties from versioning nodes inserted everywhere... if (f.tvb === 0) delete f.tvb; if (f.tvf === 0) delete f.tvf; // Remove properties used as indexes delete f.h; delete f.t; delete f.s; t.ts = f.ts; delete f.ts; if (f.hash) { t.hash = f.hash; delete f.hash; } // Remove other garbage if (f.seen) { t.seen = f.seen; delete f.seen; // inserted by the dynlist } if (f.shares) { t.shares = f.shares; delete f.shares; // will be populated from the s table } if (f.fav !== undefined && !(f.fav | 0)) { delete f.fav; } if (f.lbl !== undefined && !(f.lbl | 0)) { delete f.lbl; } if (f.p) { t.p = f.p; delete f.p; } if (f.ar) { t.ar = f.ar; delete f.ar; } if (f.u === u_handle) { t.u = f.u; f.u = '~'; } return t; }, tree: function(f) { 'use strict'; var t = {h: f.h}; delete f.h; if (f.td !== undefined && !f.td) { delete f.td; } if (f.tb !== undefined && !f.tb) { delete f.tb; } if (f.tf !== undefined && !f.tf) { delete f.tf; } if (f.tvf !== undefined && !f.tvf) { delete f.tvf; } if (f.tvb !== undefined && !f.tvb) { delete f.tvb; } if (f.lbl !== undefined && !(f.lbl | 0)) { delete f.lbl; } return t; }, ua: function(ua) { 'use strict'; delete ua.k; }, u: function(usr) { 'use strict'; delete usr.u; delete usr.ats; delete usr.name; delete usr.avatar; delete usr.presence; delete usr.lastName; delete usr.firstName; delete usr.shortName; delete usr.presenceMtime; }, mcf: function(mcf) { 'use strict'; // mcf may contain 'undefined' values, which should NOT be set, otherwise they may replace the mcfCache var cache = {}; var keys = ['id', 'cs', 'g', 'u', 'ts', 'ct', 'ck', 'f', 'm']; for (var idx = keys.length; idx--;) { var k = keys[idx]; if (mcf[k] !== undefined) { cache[k] = mcf[k]; } } FMDB._mcfCache[cache.id] = Object.assign({}, FMDB._mcfCache[mcf.id], cache); Object.assign(mcf, FMDB._mcfCache[cache.id]); var t = {id: mcf.id, ou: mcf.ou, n: mcf.n, url: mcf.url}; delete mcf.id; delete mcf.ou; delete mcf.url; delete mcf.n; if (mcf.g === 0) { t.g = 0; delete mcf.g; } if (mcf.m === 0) { t.m = 0; delete mcf.m; } if (mcf.f === 0) { t.f = 0; delete mcf.f; } if (mcf.cs === 0) { t.cs = 0; delete mcf.cs; } if (mcf.u) { t.u = mcf.u; mcf.u = ''; for (var i = t.u.length; i--;) { mcf.u += t.u[i].u + t.u[i].p; } } return t; } }); // re-add previously removed index fields to the payload object FMDB.prototype.restorenode = Object.freeze({ ok : function(ok, index) { 'use strict'; ok.h = index.h; }, f : function(f, index) { 'use strict'; f.h = index.h; f.p = index.p; f.ts = index.t < 0 ? 1262304e3 - index.t : index.t; if (index.c) { f.hash = index.c; } if (index.s < 0) f.t = -index.s; else { f.t = 0; f.s = parseFloat(index.s); } if (!f.ar && f.k && typeof f.k == 'object') { f.ar = Object.create(null); } if (f.u === '~') { f.u = u_handle; } }, tree : function(f, index) { 'use strict'; f.h = index.h; }, ph : function(ph, index) { 'use strict'; ph.h = index.h; }, ua : function(ua, index) { 'use strict'; ua.k = index.k; }, u: function(usr, index) { 'use strict'; usr.u = index.u; }, h: function(out, index) { 'use strict'; out.h = index.h; out.hash = index.c; }, mk : function(mk, index) { 'use strict'; mk.h = index.h; }, mcf: function(mcf, index) { 'use strict'; mcf.id = index.id; mcf.m = mcf.m || 0; mcf.g = mcf.g || 0; mcf.f = mcf.f || 0; mcf.cs = mcf.cs || 0; if (typeof mcf.u === 'string') { var users = []; for (var i = 0; i < mcf.u.length; i += 12) { users.push({ p: mcf.u[11 + i] | 0, u: mcf.u.substr(i, 11) }); } mcf.u = users; } FMDB._mcfCache[mcf.id] = mcf; } }); // enqueue IndexedDB puts // sn must be added last and effectively (mostly actually) "commits" the "transaction" // the next addition will then start a new "transaction" // (large writes will not execute as an IndexedDB transaction because IndexedDB can't) FMDB.prototype.add = function fmdb_add(table, row) { "use strict"; if (this.crashed) return; this.enqueue(table, row, 0); }; // enqueue IndexedDB deletions FMDB.prototype.del = function fmdb_del(table, index) { "use strict"; if (this.crashed) return; this.enqueue(table, index, 1); }; // non-transactional read with subsequent deobfuscation, with optional prefix filter // (must NOT be used for dirty reads - use getbykey() instead) FMDB.prototype.get = function fmdb_get(table, chunked) { "use strict"; var self = this; return new Promise(function(resolve, reject) { if (self.crashed > 1) { // a read operation failed previously return resolve([]); } if (d) { self.logger.log("Fetching entire table %s...", table, chunked ? '(chunked)' : ''); } if (chunked) { var limit = 8192; var keyPath = self.db[table].schema.primKey.keyPath; self.db[table].orderBy(keyPath) .limit(limit) .toArray() .then(function _(r) { if (r.length) { var last = r[r.length - 1][keyPath].slice(0); self.normaliseresult(table, r); last = chunked(r, last) || last; if (r.length >= limit) { self.db[table].where(keyPath).above(last).limit(limit).toArray().then(_).catch(reject); return; } } resolve(); }) .catch(reject); return; } self.db[table].toArray() .then(function(r) { self.normaliseresult(table, r); resolve(r); }) .catch(function(ex) { if (d && !self.crashed) { self.logger.error("Read operation failed, marking DB as read-crashed", ex); } self.invalidate(function() { resolve([]); }, 1); }); }); }; FMDB.prototype.normaliseresult = function fmdb_normaliseresult(table, r) { "use strict"; var t; for (var i = r.length; i--; ) { try { if (!r[i]) { // non-existing bulkGet result. r.splice(i, 1); continue; } if (this._raw(r[i])) { // not encrypted. if (d) { console.assert(FMDB.$usePostSerialz); } r[i] = r[i].d; continue; } t = r[i].d ? JSON.parse(this.strdecrypt(r[i].d)) : {}; if (this.restorenode[table]) { // restore attributes based on the table's indexes for (var p in r[i]) { if (p !== 'd' && (table !== 'f' || p !== 't')) { r[i][p] = this.fromStore(r[i][p]); } } this.restorenode[table](t, r[i]); } r[i] = t; } catch (ex) { if (d) { this.logger.error("IndexedDB corruption: " + this.strdecrypt(r[i].d), ex); } r.splice(i, 1); } } }; // non-transactional read with subsequent deobfuscation, with optional key filter // (dirty reads are supported by scanning the pending writes after the IndexedDB read completes) // anyof and where are mutually exclusive, FIXME: add post-anyof where filtering? // eslint-disable-next-line complexity FMDB.prototype.getbykey = promisify(function fmdb_getbykey(resolve, reject, table, index, anyof, where, limit) { 'use strict'; var bulk = false; var options = false; if (typeof index !== 'string') { options = index; index = options.index; anyof = anyof || options.anyof; where = where || options.where; limit = limit || options.limit; } if (this.crashed > 1 || anyof && !anyof[1].length) { return onIdle(reject.bind(null, [])); } var fmdb = this; var ch = fmdb.channelmap[table] || 0; if (d) { fmdb.logger.log("Fetching table %s...", table, options || where); } var t = fmdb.db[table]; var i = 0; if (!index) { // No index provided, fallback to primary key index = t.schema.primKey.keyPath; } if (anyof) { // encrypt all values in the list for (i = anyof[1].length; i--;) { anyof[1][i] = this.toStore(anyof[1][i]); } if (anyof[1].length > 1) { if (!limit && anyof[0] === t.schema.primKey.keyPath) { bulk = true; t = t.bulkGet(anyof[1]); } else { // TODO: benchmark/replace .anyOf() by Promise.all() + .equals() t = t.where(anyof[0]).anyOf(anyof[1]); } } else { t = t.where(anyof[0]).equals(anyof[1][0]); } } else if (options.query) { // Perform custom user-provided query t = options.query(t); } else if (where) { for (var k = where.length; k--;) { // encrypt the filter values (logical AND is commutative, so we can reverse the order) if (typeof where[k][1] === 'string') { if (!this._cache[where[k][1]]) { this._cache[where[k][1]] = this.toStore(where[k][1]); } where[k][1] = this._cache[where[k][1]]; } // apply filter criterion if (i) { t = t.and(where[k][0]); } else { t = t.where(where[k][0]); i = 1; } t = t.equals(where[k][1]); } } if (options.offset) { t = t.offset(options.offset); } if (limit) { t = t.limit(limit); } t = options.sortBy ? t.sortBy(options.sortBy) : t.toArray ? t.toArray() : t; t.then(function(r) { // now scan the pending elements to capture and return unwritten updates // FIXME: typically, there are very few or no pending elements - // determine if we can reduce overall CPU load by replacing the // occasional scan with a constantly maintained hash for direct lookups? var j; var f; var k; var match = false; var matches = Object.create(null); var pendingch = fmdb.pending[ch]; var tids = Object.keys(pendingch); var dbRecords = r.length; if (bulk) { for (i = r.length; i--;) { if (!r[i]) { // non-existing bulkGet result. r.splice(i, 1); } } } // iterate transactions in reverse chronological order for (var ti = tids.length; ti--;) { var tid = tids[ti]; t = pendingch[tid][table]; // any updates pending for this table? if (t && (t[t.h] && t[t.h].length || t[t.h - 1] && t[t.h - 1].length)) { // debugger // examine update actions in reverse chronological order // FIXME: can stop the loop at t.t for non-transactional writes for (var a = t.h; a >= 0; a--) { /* eslint-disable max-depth */ if (t[a]) { if (a & 1) { // deletion - always by bare index var deletions = t[a]; for (j = deletions.length; j--;) { f = fmdb._value(deletions[j]); if (typeof matches[f] == 'undefined') { // boolean false means "record deleted" if (dbRecords) { // no need to record a deletion unless we got db entries matches[f] = false; match = true; } } } } else { // addition or update - index field is attribute var updates = t[a]; // iterate updates in reverse chronological order // (updates are not commutative) for (j = updates.length; j--;) { var update = updates[j]; f = fmdb._value(update[index]); if (typeof matches[f] == 'undefined') { // check if this update matches our criteria, if any if (where) { for (k = where.length; k--;) { if (!fmdb.compare(table, where[k][0], where[k][1], update)) { break; } } // mismatch detected - record it as a deletion if (k >= 0) { // no need to record a deletion unless we got db entries if (dbRecords) { match = true; matches[f] = false; } continue; } } else if (options.query) { // If a custom query was made, notify there was a // pending update and whether if should be included. if (!(options.include && options.include(update, index))) { // nope - record it as a deletion matches[f] = false; match = true; continue; } } else if (anyof) { // does this update modify a record matched by the anyof inclusion list? for (k = anyof[1].length; k--;) { if (fmdb.compare(table, anyof[0], anyof[1][k], update)) { break; } } // no match detected - record it as a deletion if (k < 0) { // no need to record a deletion unless we got db entries if (dbRecords) { match = true; matches[f] = false; } continue; } } match = true; matches[f] = update; } } } } } } } // scan the result for updates/deletions/additions arising out of the matches found if (match) { if (d) { fmdb.logger.debug('pending matches', $.len(matches)); } for (i = r.length; i--;) { // if this is a binary key, convert it to string f = fmdb._value(r[i][index]); if (typeof matches[f] !== 'undefined') { if (matches[f] === false) { // a returned record was deleted or overwritten with // keys that fall outside our where clause r.splice(i, 1); } else { // a returned record was overwritten and still matches // our where clause r[i] = fmdb.clone(matches[f]); delete matches[f]; } } } // now add newly written records for (t in matches) { if (matches[t]) { r.push(fmdb.clone(matches[t])); } } } // filter out matching records if (where) { for (i = r.length; i--;) { for (k = where.length; k--;) { if (!fmdb.compare(table, where[k][0], where[k][1], r[i])) { r.splice(i, 1); break; } } } if ($.len(fmdb._cache) > 200) { fmdb._cache = Object.create(null); } } // Apply user-provided filtering, if any if (options.filter) { r = options.filter(r); } if (r.length) { fmdb.normaliseresult(table, r); } resolve(r); }).catch(function(ex) { if (d && !fmdb.crashed) { fmdb.logger.error("Read operation failed, marking DB as read-crashed", ex); } fmdb.invalidate(function() { reject([]); }, 1); }); }); // invokes getbykey in chunked mode FMDB.prototype.getchunk = promisify(function(resolve, reject, table, options, onchunk) { 'use strict'; var self = this; if (typeof options === 'function') { onchunk = options; options = Object.create(null); } options.limit = options.limit || 1e4; var mng = options.offset === undefined; if (mng) { options.offset = -options.limit; } (function _next() { if (mng) { options.offset += options.limit; } self.getbykey(table, options) .always(function(r) { var limit = options.limit; if (onchunk(r) === false) { return resolve(EAGAIN); } return r.length >= limit ? _next() : resolve(); }); })(); }); // simple/fast/non-recursive object cloning FMDB.prototype.clone = function fmdb_clone(o) { 'use strict'; // In Firefox, Object.assign() is ~63% faster than for..in, ~21% in Chrome return Object.assign({}, o); }; /** * Encrypt 32-bit words using AES ECB... * @param {sjcl.cipher.aes} cipher AES cipher * @param {TypedArray} input data * @returns {ArrayBuffer} encrypted data * @private */ FMDB.prototype._crypt = function(cipher, input) { 'use strict'; var a32 = new Int32Array(input.buffer); var i32 = new Int32Array(a32.length + 3 & ~3); for (var i = 0; i < a32.length; i += 4) { var u = cipher.encrypt([a32[i], a32[i + 1], a32[i + 2], a32[i + 3]]); i32[i] = u[0]; i32[i + 1] = u[1]; i32[i + 2] = u[2]; i32[i + 3] = u[3]; } return i32.buffer; }; /** * Decrypt 32-bit words using AES ECB... * @param {sjcl.cipher.aes} cipher AES cipher * @param {ArrayBuffer} buffer Encrypted data * @returns {ArrayBuffer} decrypted data * @private */ FMDB.prototype._decrypt = function(cipher, buffer) { 'use strict'; var u32 = new Uint32Array(buffer); for (var i = 0; i < u32.length; i += 4) { var u = cipher.decrypt([u32[i], u32[i + 1], u32[i + 2], u32[i + 3]]); u32[i + 3] = u[3]; u32[i + 2] = u[2]; u32[i + 1] = u[1]; u32[i] = u[0]; } return u32.buffer; }; /** * Converts UTF-8 string to Unicode * @param {ArrayBuffer} buffer Input buffer * @returns {String} Unicode string. */ FMDB.prototype.from8 = function(buffer) { 'use strict'; var idx = 0; var str = ''; var ptr = new Uint8Array(buffer); var len = ptr.byteLength; while (len > idx) { var b = ptr[idx++]; if (!b) { return str; } if (!(b & 0x80)) { str += String.fromCharCode(b); continue; } var l = ptr[idx++] & 63; if ((b & 0xE0) === 0xC0) { str += String.fromCharCode((b & 31) << 6 | l); continue; } var h = ptr[idx++] & 63; if ((b & 0xF0) === 0xE0) { b = (b & 15) << 12 | l << 6 | h; } else { b = (b & 7) << 18 | l << 12 | h << 6 | ptr[idx++] & 63; } if (b < 0x10000) { str += String.fromCharCode(b); } else { var ch = b - 0x10000; str += String.fromCharCode(0xD800 | ch >> 10, 0xDC00 | ch & 0x3FF); } } return str; }; /** * Converts Unicode string to UTF-8 * @param {String} str Input string * @param {Number} [len] bytes to allocate * @returns {Uint8Array} utf-8 bytes */ FMDB.prototype.to8 = function(str, len) { 'use strict'; var p = 0; var u8 = new Uint8Array((len || this.utf8length(str)) + 3 & ~3); for (var i = 0; i < str.length; ++i) { var u = str.charCodeAt(i); if (u > 55295 && u < 57344) { u = 0x10000 + ((u & 0x3FF) << 10) | str.charCodeAt(++i) & 0x3FF; } if (u < 128) { u8[p++] = u; } else if (u < 2048) { u8[p++] = 0xC0 | u >> 6; u8[p++] = 0x80 | u & 63; } else if (u < 65536) { u8[p++] = 0xE0 | u >> 12; u8[p++] = 0x80 | u >> 6 & 63; u8[p++] = 0x80 | u & 63; } else { u8[p++] = 0xF0 | u >> 18; u8[p++] = 0x80 | u >> 12 & 63; u8[p++] = 0x80 | u >> 6 & 63; u8[p++] = 0x80 | u & 63; } } return u8; }; /** * Calculate the length required for Unicode to UTF-8 conversion * @param {String} str Input string. * @returns {Number} The length */ FMDB.prototype.utf8length = function(str) { 'use strict'; var len = 0; for (var i = 0; i < str.length; ++i) { var u = str.charCodeAt(i); if (u > 55295 && u < 57344) { u = 0x10000 + ((u & 0x3FF) << 10) | str.charCodeAt(++i) & 0x3FF; } if (u < 128) { ++len; } else if (u < 2048) { len += 2; } else if (u < 65536) { len += 3; } else { len += 4; } } return len; }; /** * Check for needle into haystack * @param {Array} haystack haystack * @param {String|ArrayBuffer} needle needle * @returns {Boolean} whether is found. */ FMDB.prototype.exists = function(haystack, needle) { 'use strict'; for (var i = haystack.length; i--;) { if (this.equal(haystack[i], needle)) { return true; } } return false; }; /** * Check whether two indexedDB-stored values are equal. * @param {ArrayBuffer|String} a1 first item to compare * @param {ArrayBuffer|String} a2 second item to compere * @returns {Boolean} true if both are deep equal */ FMDB.prototype.equal = function(a1, a2) { 'use strict'; return !indexedDB.cmp(a1, a2); }; // See {@link FMDB.equal} FMDB.prototype.equals = function(a1, a2) { 'use strict'; return a1 === a2; }; /** * Validate store-ready entry * @param {Object} entry An object * @returns {Boolean} whether it is.. * @private */ FMDB.prototype._raw = function(entry) { 'use strict'; return entry.d && entry.d.byteLength === undefined; }; /** * Get raw value for encrypted record. * @param {String|ArrayBuffer} value Input * @returns {String} raw value * @private */ FMDB.prototype._value = function(value) { 'use strict'; if (value instanceof ArrayBuffer) { value = this.fromStore(value.slice(0)); } return value; }; /** * store-agnostic value comparison. * @param {String} table The DB table * @param {String} key The row index. * @param {String|ArrayBuffer} value item to compare (always encrypted) * @param {Object} store Store containing key. (*may* not be encrypted) * @returns {Boolean} true if both are deep equal */ FMDB.prototype.compare = function(table, key, value, store) { 'use strict'; var eq = store[key]; if (!this._raw(store)) { // It's also encrypted if (d) { console.assert(typeof value === typeof eq); } return this.equal(value, eq); } if (!this._cache[eq]) { this._cache[eq] = this.toStore(eq); } return this.equal(value, this._cache[eq]); }; /** * indexedDB data serialization. * @param {String} data Input string * @returns {*|String} serialized data (as base64, unless we have binary keys support) */ FMDB.prototype.toStore = function(data) { 'use strict'; return ab_to_base64(this.strcrypt(data)); }; /** * indexedDB data de-serialization. * @param {TypedArray|ArrayBuffer|String} data Input data * @returns {*} de-serialized data. */ FMDB.prototype.fromStore = function(data) { 'use strict'; return this.strdecrypt(base64_to_ab(data)); }; // convert to encrypted-base64 FMDB.prototype.toB64 = FMDB.prototype.toStore; // convert from encrypted-base64 FMDB.prototype.fromB64 = FMDB.prototype.fromStore; if (FMDB.$useBinaryKeys) { FMDB.prototype.toStore = FMDB.prototype.strcrypt; FMDB.prototype.fromStore = FMDB.prototype.strdecrypt; } else { FMDB.prototype.equal = FMDB.prototype.equals; if (!FMDB.$usePostSerialz) { FMDB.prototype.compare = FMDB.prototype.equal; console.warn('Fix FMDB._value()....'); } } if (!FMDB.$usePostSerialz) { FMDB.prototype.add = function fmdb_adds(table, row) { 'use strict'; if (!this.crashed) { this.enqueue(table, this.serialize(table, row), 0); } }; FMDB.prototype.del = function fmdb_dels(table, index) { "use strict"; if (!this.crashed) { this.enqueue(table, this.toStore(index), 1); } }; } // @private FMDB.prototype._bench = function(v, m) { 'use strict'; var i; var a = Array(1e6); var s = '\u0073\u0061\u006d\u0070\u006c\u0065\u0020\u0074\u0072\u0061\u006e\u0073\u006c\u0061' + '\u0074\u0065\u003a\u0020\u5c11\u91cf\u002c\u0020\u6837\u54c1\u002c\u0020\uff08\u533b' + '\u751f\u6216\u79d1\u5b66\u5bb6\u68c0\u6d4b\u7528\u7684\uff09\u6837\u672c\uff0c\u8bd5'; s = typeof v === 'string' ? v : v && JSON.stringify(v) || s; var enc = 'strcrypt' + (m === undefined ? '' : m); var dec = 'strdecrypt' + (m === undefined ? '' : m); onIdle(function() { console.time(enc); for (i = a.length; i--;) { a[i] = fmdb[enc](s); } console.timeEnd(enc); }); onIdle(function() { console.time(dec); for (i = a.length; i--;) { fmdb[dec](a[i]); } console.timeEnd(dec); }); onIdle(function() { console.assert(m !== undefined || fmdb.from8(a[1]).split('\0')[0] === s); console.groupEnd(); }); console.group('please wait...'); }; // reliably invalidate the current database (delete the sn) FMDB.prototype.invalidate = function fmdb_invalidate(cb, readop) { cb = cb || this.logger.debug.bind(this.logger, 'DB Invalidation', !this.crashed); if (this.crashed) { return cb(); } var channels = Object.keys(this.pending); // erase all pending data for (var i = channels.length; i--; ) { this.head[i] = 0; this.tail[i] = 0; this.pending[i] = Object.create(null); } // clear the writing flag for the next del() call to pass through this.writing = null; // enqueue the final _sn deletion that will mark the DB as invalid this.del('_sn', 1); // prevent further reads or writes this.crashed = readop ? 2 : 1; // set completion callback this.inval_cb = function() { // XXX: Just invalidating the DB may causes a timeout trying to open it on the next page load, since we // do attempt to delete it when no sn is found, which would take a while to complete for large accounts. // This is currently the 20% of hits we do receive through 99724 so from now on we will hold the current // session until the DB has been deleted. if (readop || !this.db) { onIdle(cb); } else { this.db.delete().then(cb).catch(cb); } document.documentElement.classList.remove('fmdb-working'); }; // force a non-treecache on the next load // localStorage.force = 1; }; // checks if crashed or being used by another tab concurrently FMDB.prototype.up = function fmdb_up() { return !this.crashed; /* if (this.crashed) return false; var state = localStorage[this.name]; var time = Date.now(); // another tab was active within the last second? if (state) { state = JSON.parse(state); if (time-state[0] < 1000 && state[1] !== this.identity) { this.crashed = true; this.logger.error("*** DISCONNECTING FROM INDEXEDDB - cross-tab interference detected"); // FIXME: check if mem-only ops are safe at this point, force reload if not return false; } } localStorage[this.name] = '[' + time + ',"' + this.identity + '"]'; return true; */ }; // FIXME: improve like this: // when a new tab is opened with the same session, request an update freeze from the master tab // once the loading is completed, relinquish update lock // (we could do this with transactions if Safari supported them properly...) FMDB.prototype.beacon = function fmdb_beacon() { if (this.up()) { setTimeout(this.beacon.bind(this), 500); } }; function mDBcls() { if (fmdb && fmdb.db) { fmdb.db.close(); } fmdb = null; } // -------------------------------------------------------------------------- /** * Wrapper around Dexie that remembers and removes deprecated databases. * @param {String} aUniqueID Unique Identified for this database. * @see {@link MegaDexie.getDBName} for additional parameters. * @details as part of this constructor, {aPayLoad} must be the schema if provided. * @constructor */ function MegaDexie(aUniqueID) { 'use strict'; var __args = toArray.apply(null, arguments).slice(1); var dbname = MegaDexie.getDBName.apply(this, __args); this.__dbUniqueID = this.__fromUniqueID(aUniqueID + __args[0]); this.__rememberDBName(dbname); Dexie.call(this, dbname); if (__args[3]) { // Schema given. this.version(1).stores(__args[3]); } if (d > 2) { MegaDexie.__dbConnections.push(this); } } inherits(MegaDexie, Dexie); /** * Helper to create common database names. * @param {String} aName The main name. * @param {String} [aIdent] Identity (added to the db name as-is) * @param {Boolean} [aTagUser] Whether this database is user-specific * @param {*} [aPayload] Some serializable data to randomize the db name * @param {*} [aPersistent] Whether this database is persistent, true by default. * @returns {String} encrypted database name */ MegaDexie.getDBName = function(aName, aIdent, aTagUser, aPayload, aPersistent) { 'use strict'; if (typeof u_k_aes === 'undefined') { if (window.u_k) { throw new Error('Invalid account state.'); } window.u_k_aes = new sjcl.cipher.aes(str_to_a32('' + ua).slice(-4)); console.warn('MegaDexie.getDBName: Adding temporal key for non-logged user.'); } var pex = aPersistent === false ? '' : FMDB.perspex; aPayload = aPayload && MurmurHash3(JSON.stringify(aPayload)).toString(16) || ''; return (aIdent || '') + FMDB.prototype.toB64(aPayload + aName + (aTagUser ? u_handle : '')) + pex; }; /** * The missing Dexie.bulkUpdate * @param {String} [table] Optional Dexie table * @param {Array} bulkData Bulk data * @returns {Promise} promise */ MegaDexie.prototype.bulkUpdate = promisify(function(resolve, reject, table, bulkData) { 'use strict'; if (typeof table !== 'string') { bulkData = table; table = this.tables; table = table.length === 1 && table[0].name; } if (!bulkData.length) { return resolve(bulkData); } table = this.table(table); var i; var keyPath; var anyOf = []; var schema = table.schema; var indexes = schema.indexes; for (i = 0; i < indexes.length; ++i) { if (indexes[i].unique) { keyPath = indexes[i].keyPath; break; } } for (i = bulkData.length; i--;) { var v = bulkData[i][keyPath || schema.primKey.keyPath]; if (FMDB.prototype.exists(anyOf, v)) { bulkData.splice(i, 1); } else { anyOf.push(v); } } (keyPath ? table.where(keyPath).anyOf(anyOf).toArray() : table.bulkGet(anyOf)) .then(function(r) { var toUpdate = []; keyPath = keyPath || schema.primKey.keyPath; for (var i = r.length; i--;) { for (var j = r[i] && bulkData.length; j--;) { if (FMDB.prototype.equal(r[i][keyPath], bulkData[j][keyPath])) { delete bulkData[j][keyPath]; toUpdate.push([r[i], bulkData.splice(j, 1)[0]]); break; } } } var tasks = toUpdate.map(function(u) { return table.where(":id").equals(u[0][schema.primKey.keyPath]).modify(u[1]); }); if (bulkData.length) { tasks.push(table.bulkPut(bulkData)); } return Promise.all(tasks); }) .then(resolve) .catch(reject); }); /** * Remember newly opened database. * @param {String} aDBName The database being opened. * @returns {void} * @private */ MegaDexie.prototype.__rememberDBName = function(aDBName) { 'use strict'; var aUniqueID = this.__dbUniqueID; MegaDexie.__knownDBNames.get(aUniqueID) .then(function(s) { if (s) { if (s.v === aDBName) { return; } Dexie.delete(s.v).then(nop).catch(dump); } return MegaDexie.__knownDBNames.put({k: aUniqueID, t: Date.now() - 1589e9, v: aDBName}); }) .catch(nop); this.__checkStaleDBNames(); }; /** * Forget database. * @returns {void} * @private */ MegaDexie.prototype.__forgetDBName = function() { 'use strict'; MegaDexie.__knownDBNames.delete(this.__dbUniqueID).then(nop).catch(dump); }; MegaDexie.prototype.__checkStaleDBNames = function() { 'use strict'; if (MegaDexie.__staleDBsChecked) { return; } var canQueryDatabases = typeof Object(window.indexedDB).databases === 'function'; MegaDexie.__staleDBsChecked = canQueryDatabases ? true : -1; if (canQueryDatabases) { setTimeout(function() { var databases = []; if (d) { console.debug('Checking stale databases...'); } indexedDB.databases() .then(function(r) { for (var i = r.length; i--;) { console.assert(r[i].name); if (r[i].name) { databases.push(r[i].name); } } return databases.length ? MegaDexie.__knownDBNames.toArray() : Promise.resolve([]); }) .then(function(r) { var stale = []; for (var i = r.length; i--;) { if (databases.indexOf(r[i].v) < 0) { if (d) { console.warn('Found stale database...', r[i].v); } stale.push(r[i].k); } } if (stale.length) { MegaDexie.__knownDBNames.bulkDelete(stale).then(nop).catch(dump); } else { console.debug('Yay, no stale databases found.'); } }) .catch(nop); }, 4e4); } }; /** * Hash unique identifier * @param {Number|String} aUniqueID Unique Identifier for database. * @returns {Number} hash * @private */ MegaDexie.prototype.__fromUniqueID = function(aUniqueID) { 'use strict'; return MurmurHash3('mega' + aUniqueID + window.u_handle, -0x9fffee); }; /** * Deletes the database. * @returns {Promise} promise */ MegaDexie.prototype.delete = function() { 'use strict'; this.__forgetDBName(); return Dexie.prototype.delete.apply(this, arguments); }; /** * Open the database. * @returns {Promise} promise */ MegaDexie.prototype.open = function() { 'use strict'; this.__rememberDBName(this.name); return Dexie.prototype.open.apply(this, arguments); }; /** * @name __knownDBNames * @memberOf MegaDexie */ lazy(MegaDexie, '__knownDBNames', function() { 'use strict'; var db = new Dexie('$kdbn', {addons: []}); db.version(1).stores({k: '&k'}); return db.table('k'); }); // @private MegaDexie.__dbConnections = []; /** * Creates a new database layer, which may change at anytime, and thus with * the only assertion the instance returned will have set/get/remove methods * @param {String} name Database name. * @param {Boolean|Number} binary mode * @returns {*} database instance. */ MegaDexie.create = function(name, binary) { 'use strict'; binary = binary && SharedLocalKVStorage.DB_MODE.BINARY; return new SharedLocalKVStorage.Utils.DexieStorage('mdcdb:' + name, binary); }; // Remove obsolete databases. mBroadcaster.once('startMega', tryCatch(function _removeObsoleteDatabases() { 'use strict'; if (Date.now() > 163e10) { console.error('Remove me \uD83D\uDD34'); return; } Dexie.getDatabaseNames() .then(function(r) { for (var i = r.length; i--;) { var n = r[i]; if (n.substr(0, 6) === '$ctdb_') { Dexie.delete(n).then(nop).catch(dump); console.info('Removing obsolete db:%s', n); } } }) .catch(nop); var entries = Object.keys(localStorage) .filter(function(k) { return k.startsWith('_$mdb$'); }); for (var i = entries.length; i--;) { delete localStorage[entries[i]]; } }, false)); // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Helper functions to retrieve nodes from indexedDB. * @name dbfetch * @memberOf window */ Object.defineProperty(self, 'dbfetch', (function() { 'use strict'; var tree_inflight = Object.create(null); var node_inflight = Object.create(null); var getNode = function(h, cb) { if (M.d[h]) { return cb(M.d[h]); } fmdb.getbykey('f', 'h', ['h', [h]]) .always(function(r) { if (r.length) { emplacenode(r[0], true); } cb(M.d[h]); }); }; var showLoading = function(h) { $.dbOpenHandle = h; document.documentElement.classList.add('wait-cursor'); }; var hideLoading = function(h) { if ($.dbOpenHandle === h) { $.dbOpenHandle = false; document.documentElement.classList.remove('wait-cursor'); } }; var dbfetch = Object.freeze({ /** * Retrieve root nodes only, on-demand node loading mode. * or retrieve whole 'f' table in chunked mode. * @returns {Promise} fulfilled on completion * @memberOf dbfetch */ init: function fetchfroot() { if (!mBroadcaster.crossTab.master) { // fetch the whole cloud on slave tabs.. return fmdb.get('f', function(r) { for (var i = r.length; i--;) { emplacenode(r[i]); } }); } return new Promise(function(resolve, reject) { // fetch the three root nodes fmdb.getbykey('f', 'h', ['s', ['-2', '-3', '-4']]).always(function(r) { for (var i = r.length; i--;) { emplacenode(r[i]); } if (!r.length || !M.RootID) { return reject('indexedDB corruption!'); } // fetch all top-level nodes fmdb.getbykey('f', 'h', ['p', [M.RootID, M.InboxID, M.RubbishID]]) .always(function(r) { for (var i = r.length; i--;) { emplacenode(r[i]); } resolve(); }); }); }); }, /** * Check whether a node is currently loading from DB. * @param {String} handle The ufs-node handle * @returns {Boolean} whether it is. */ isLoading: function(handle) { return Boolean(tree_inflight[handle] || node_inflight[handle]); }, /** * Fetch all children; also, fetch path to root; populates M.c and M.d in streaming mode * * @param {String} handle Node handle * @param {MegaPromise} [waiter] waiting parent * @returns {*|MegaPromise} * @memberOf dbfetch */ open: promisify(function(resolve, reject, handle, waiter) { var fail = function(ex) { reject(ex); hideLoading(handle); queueMicrotask(function() { if (tree_inflight[handle] === waiter) { delete tree_inflight[handle]; } waiter.reject(ex); }); }; var done = function(res) { if (resolve) { resolve(res); } hideLoading(handle); queueMicrotask(function() { if (tree_inflight[handle] === waiter) { delete tree_inflight[handle]; } waiter.resolve(handle); }); }; var ready = function(n) { return dbfetch.open(n.p); }; if (typeof handle !== 'string' || handle.length !== 8) { return resolve(handle); } var silent = waiter === undefined; if (silent) { // @todo refactor all of dbfetch to not use MegaPromise... // eslint-disable-next-line local-rules/hints waiter = new MegaPromise(); } else { showLoading(handle); } if (tree_inflight[handle]) { if (M.c[handle]) { queueMicrotask(resolve); resolve = null; } return tree_inflight[handle].then(done).catch(fail); } tree_inflight[handle] = waiter; getNode(handle, function(n) { if (!n) { return fail(ENOENT); } if (!n.t || M.c[n.h]) { return ready(n).always(done); } var promise; var opts = { limit: 4, offset: 0, where: [['p', handle]] }; if (d) { opts.i = makeid(9) + '.' + handle; } if (!silent) { showLoading(handle); } fmdb.getchunk('f', opts, function(r) { if (!opts.offset) { M.c[n.h] = Object.create(null); } opts.offset += opts.limit; if (opts.limit < 4096) { opts.limit <<= 2; } for (var i = r.length; i--;) { emplacenode(r[i]); } if (ready) { promise = ready(n).always(resolve); ready = resolve = null; } else { promise.always(function() { newnodes = newnodes.concat(r); queueMicrotask(function() { M.updFileManagerUI().dump('dbf-open-' + opts.i); }); }); } }).always(function() { promise.always(done); }); }); }), /** * Fetch all children; also, fetch path to root; populates M.c and M.d * * @param {String} parent Node handle * @param {MegaPromise} [promise] * @returns {*|MegaPromise} * @memberOf dbfetch */ get: function fetchchildren(parent, promise) { if (d > 1) { console.warn('fetchchildren', parent, promise); } promise = promise || MegaPromise.busy(); if (typeof parent !== 'string') { if (d) { console.warn('Invalid parent, cannot fetchchildren', parent); } promise.reject(EARGS); } else if (!fmdb) { if (d) { console.debug('No fmdb available...', folderlink, pfid); } promise.reject(EFAILED); } // is this a user handle or a non-handle? no fetching needed. else if (parent.length != 8) { promise.resolve(); } // has the parent been fetched yet? else if (!M.d[parent]) { fmdb.getbykey('f', 'h', ['h', [parent]]) .always(function(r) { if (r.length > 1) { console.error('Unexpected number of result for node ' + parent, r.length, r); } for (var i = r.length; i--;) { // providing a 'true' flag so that the node isn't added to M.c, // otherwise crawling back to the parent won't work properly. emplacenode(r[i], true); } if (!M.d[parent]) { // no parent found?! promise.reject(ENOENT); } else { dbfetch.get(parent, promise); } }); } // have the children been fetched yet? else if (M.d[parent].t && !M.c[parent]) { // no: do so now. this.tree([parent], 0, new MegaPromise()) .always(function() { if (M.d[parent] && M.c[parent]) { dbfetch.get(M.d[parent].p, promise); } else { console.error('Failed to load folder ' + parent); api_req({a: 'log', e: 99667, m: 'Failed to fill M.c for a folder node..'}); promise.reject(EACCESS); } }); } else { // crawl back to root (not necessary until we start purging from memory) dbfetch.get(M.d[parent].p, promise); } return promise; }, /** * Fetch all children; also, fetch path to root; populates M.c and M.d * same as fetchchildren/dbfetch.get, but takes an array of handles. * * @param {Array} handles * @param {MegaPromise} [promise] * @returns {MegaPromise} * @memberOf dbfetch */ geta: function geta(handles, promise) { promise = promise || MegaPromise.busy(); var promises = []; for (var i = handles.length; i--;) { // fetch nodes and their path to root promises.push(dbfetch.get(handles[i], new MegaPromise())); } promise.linkDoneAndFailTo(MegaPromise.allDone(promises)); return promise; }, /** * Fetch entire subtree. * * @param {Array} parents Node handles * @param {Number} [level] Recursion level, optional * @param {MegaPromise} [promise] optional * @param {Array} [handles] -- internal use only * @returns {*|MegaPromise} * @memberOf dbfetch */ tree: function fetchsubtree(parents, level, promise, handles) { var p = []; var inflight = Object.create(null); if (level === undefined) { level = -1; } // setup promise promise = promise || MegaPromise.busy(); if (!fmdb) { if (d) { console.debug('No fmdb available...', folderlink, pfid); } return promise.reject(EFAILED); } // first round: replace undefined handles with the parents if (!handles) { handles = parents; } // check which parents have already been fetched - no need to fetch those // (since we do not purge loaded nodes, the presence of M.c for a node // means that all of its children are guaranteed to be in memory.) for (var i = parents.length; i--;) { if (tree_inflight[parents[i]]) { inflight[parents[i]] = tree_inflight[parents[i]]; } else if (!M.c[parents[i]]) { p.push(parents[i]); tree_inflight[parents[i]] = promise; } } var masterPromise = promise; if ($.len(inflight)) { masterPromise = MegaPromise.allDone(array.unique(obj_values(inflight)).concat(promise)); } // console.warn('fetchsubtree', arguments, p, inflight); // fetch children of all unfetched parents fmdb.getbykey('f', 'h', ['p', p.concat()]) .always(function(r) { // store fetched nodes for (var i = p.length; i--;) { delete tree_inflight[p[i]]; // M.c should be set when *all direct* children have // been fetched from the DB (even if there are none) M.c[p[i]] = Object.create(null); } for (var i = r.length; i--;) { emplacenode(r[i]); } if (level--) { // extract parents from children p = []; for (var i = parents.length; i--;) { for (var h in M.c[parents[i]]) { handles.push(h); // with file versioning, files can have children, too! if (M.d[h].t || M.d[h].tvf) { p.push(h); } } } if (p.length) { fetchsubtree(p, level, promise, handles); return; } } promise.resolve(); }); return masterPromise; }, /** * Retrieve nodes by handle. * WARNING: emplacenode() is not used, it's up to the caller if so desired. * * @param {Array} handles * @returns {MegaPromise} * @memberOf dbfetch */ node: promisify(function fetchnode(resolve, reject, handles) { var result = []; for (var i = handles.length; i--;) { if (M.d[handles[i]]) { result.push(M.d[handles[i]]); handles.splice(i, 1); } } if (!handles.length || !fmdb) { if (d && handles.length) { console.warn('Unknown nodes: ' + handles); } return resolve(result); } fmdb.getbykey('f', 'h', ['h', handles.concat()]) .always(function(r) { if (handles.length == 1 && r.length > 1) { console.error('Unexpected DB reply, more than a single node returned.'); } for (var i = handles.length; i--;) { delete node_inflight[handles[i]]; } resolve(result.concat(r)); }); }), /** * Retrieve a node by its hash. * * @param hash * @returns {MegaPromise} * @memberOf dbfetch */ hash: function fetchhash(hash) { var promise = new MegaPromise(); if (M.h[hash] && Object.keys(M.h[hash])[0]) { promise.resolve(Object.keys(M.h[hash])[0]); } else { fmdb.getbykey('f', 'c', false, [['c', hash]], 1) .always(function(r) { var node = r[0]; if (node) { // got the hash and a handle it belong to if (!M.h[hash]) { M.h[node.hash] = Object.create(null); M.h[node.hash][node.h] = true; } else { if (!M.h[node.hash][node.h]) { M.h[node.hash][node.h] = true; } } promise.resolve(node); } else { promise.resolve(); } }); } return promise; }, /** * Fetch all children recursively; also, fetch path to root * * @param {Array} handles * @param {MegaPromise} [promise] * @returns {*|MegaPromise} * @memberOf dbfetch */ coll: function fetchrecursive(handles, promise) { promise = promise || MegaPromise.busy(); if (!fmdb) { promise.resolve(); return promise; } // fetch nodes and their path to root this.geta(handles, new MegaPromise()) .always(function() { var folders = []; for (var i = handles.length; i--;) { var h = handles[i]; if (M.d[h] && (M.d[h].t || M.d[h].tvf)) { folders.push(h); } } if (folders.length) { dbfetch.tree(folders, -1, new MegaPromise()) .always(function(r) { promise.resolve(r); }); } else { promise.resolve(); } }); return promise; } }); return {value: dbfetch}; })()); /* Collect entropy from mouse motion and key press events * Note that this is coded to work with either DOM2 or Internet Explorer * style events. * We don't use every successive mouse movement event. * Instead, we use some bits from random() to determine how many * subsequent mouse movements we ignore before capturing the next one. * * Collected entropy is used to salt asmCrypto's PRNG. * * mouse motion event code originally from John Walker * key press timing code thanks to Nigel Johnstone */ var lastactive = Date.now(); var bioSeed = new Uint32Array(256); var bioCounter = 0; var mouseMoveSkip = 0; // Delay counter for mouse entropy collection // ---------------------------------------- if (window.performance !== undefined && window.performance.now !== undefined) { // Though `performance.now()` SHOULD be accurate to a microsecond, // spec says it's implementation-dependant (http://www.w3.org/TR/hr-time/#sec-DOMHighResTimeStamp) // That's why 16 least significan bits are returned var timeValue = function() { return (window.performance.now() * 1000) & 0xffff }; } else { if (d) { console.warn("Entropy collector uses low-precision Date.now()"); } var timeValue = function() { return Date.now() & 0xffff }; } function keyPressEntropy(e) { 'use strict'; lastactive = Date.now(); bioSeed[bioCounter++ & 255] ^= (e.keyCode << 16) | timeValue(); if (typeof onactivity === 'function') { delay('ev:on.activity', onactivity, 800); } } var mouseApiRetryT = false; function mouseMoveEntropy(e) { 'use strict'; lastactive = Date.now(); var v = (((e.screenX << 8) | (e.screenY & 255)) << 16) | timeValue(); if (saveRandSeed.needed) { if (bioCounter < 45) { // `bioCounter` is incremented once per 4 move events in average // 45 * 4 = 180 first move events should provide at about 270 bits of entropy // (conservative estimation is 1.5 bits of entropy per move event) asmCrypto.random.seed(new Uint32Array([v])); } else { if (d) { console.log("Got the first seed for future PRNG reseeding"); } saveRandSeed(); } } if (mouseMoveSkip-- <= 0) { bioSeed[bioCounter++ & 255] ^= v; mouseMoveSkip = (Math.random() * 8) | 0; if ((bioCounter & 255) === 0) { if (d) { console.log("Reseeding PRNG with collected entropy"); } asmCrypto.random.seed(bioSeed); saveRandSeed(); } } if (!mouseApiRetryT || mouseApiRetryT < lastactive) { mouseApiRetryT = lastactive + 2000; api_retry(); } if (typeof onactivity === 'function') { delay('ev:on.activity', onactivity, 700); } } // Store some random bits for reseeding RNG in the future function saveRandSeed() { 'use strict'; var randseed = new Uint8Array(32); asmCrypto.getRandomValues(randseed); localStorage.randseed = base64urlencode(asmCrypto.bytes_to_string(randseed)); saveRandSeed.needed = false; } saveRandSeed.needed = !localStorage.randseed; // ---------------------------------------- function eventsEnd() { if (document.removeEventListener) { document.removeEventListener("mousemove", mouseMoveEntropy, false); document.removeEventListener("keypress", keyPressEntropy, false); } else if (document.detachEvent) { document.detachEvent("onmousemove", mouseMoveEntropy); document.detachEvent("onkeypress", keyPressEntropy); } } // Start collection of entropy. function eventsCollect() { 'use strict'; if (!d) { asmCrypto.random.skipSystemRNGWarning = true; } if (localStorage.randseed) { if (d) { console.log("Initially seeding PRNG with a stored seed"); } asmCrypto.random.seed(asmCrypto.string_to_bytes(base64urldecode(localStorage.randseed))); } if (mega.getRandomValues.strong) { if (d > 1) { console.log("Initially seeding PRNG with strong random values"); } asmCrypto.random.seed(mega.getRandomValues(384)); } if ((document.implementation.hasFeature("Events", "2.0")) && document.addEventListener) // Document Object Model (DOM) 2 events { document.addEventListener("mousemove", mouseMoveEntropy, false); document.addEventListener("keypress", keyPressEntropy, false); } else if (document.attachEvent) // IE 5 and above event model { document.attachEvent("onmousemove", mouseMoveEntropy); document.attachEvent("onkeypress", keyPressEntropy); } } // keyboard/mouse entropy mBroadcaster.once('boot_done', eventsCollect); /** * MEGA Data Structures * Modern/unified way of handling/monitoring/syncing data changes required for the Webclient to work in a more: * 1. easy to use by us (developers) * 2. reactive/optimised way */ (function _dataStruct(global) { 'use strict'; var dsIncID = 0; var VALUE_DESCRIPTOR = {configurable: true, value: null}; function _defineValue(target, prop, value) { VALUE_DESCRIPTOR.value = value; Object.defineProperty(target, prop, VALUE_DESCRIPTOR); VALUE_DESCRIPTOR.value = null; } var VNE_DESCRIPTOR = {configurable: true, writable: true, value: null}; function _defineNonEnum(target, prop, value) { VNE_DESCRIPTOR.value = value; Object.defineProperty(target, prop, VNE_DESCRIPTOR); VNE_DESCRIPTOR.value = null; } function _cmp(a, b) { return a === b || a === null && b === undefined || b === null && a === undefined; } function _timing(proto, min, max) { min = min || 10; max = max || 70; var wrap = function(f, m) { return function() { var t = performance.now(); var r = m.apply(this, arguments); if ((t = performance.now() - t) > min) { var fn = t > max ? 'error' : 'warn'; console[fn]('[timing] %s.%s: %fms', this, f, t, [this], toArray.apply(null, arguments)); } return r; }; }; proto = proto.prototype || proto; var keys = Object.keys(proto); console.warn('timing %s...', Object(proto.constructor).name || '', keys); for (var i = keys.length; i--;) { if (typeof proto[keys[i]] === 'function') { proto[keys[i]] = wrap(keys[i], proto[keys[i]]); } } } var _warnOnce = SoonFc(400, function _warnOnce(where) { var args = toArray.apply(null, arguments).slice(1); var prop = '__warn_once_' + MurmurHash3(args[0], -0x7ff); if (!where[prop]) { _defineNonEnum(where, prop, 1); console.warn.apply(console, args); } }); function returnFalse() { return false; } function returnTrue() { return true; } function MegaDataEvent(src, target) { if (typeof src === 'object') { this.originalEvent = src; if (src.defaultPrevented || src.defaultPrevented === undefined && src.returnValue === false || src.isDefaultPrevented && src.isDefaultPrevented()) { this.isDefaultPrevented = returnTrue; } } else { src = {type: src}; } this.type = src.type; this.target = src.target || target; } inherits(MegaDataEvent, null); MegaDataEvent.prototype.isDefaultPrevented = returnFalse; MegaDataEvent.prototype.isPropagationStopped = returnFalse; MegaDataEvent.prototype.preventDefault = function() { this.isDefaultPrevented = returnTrue; if (this.originalEvent) { this.originalEvent.preventDefault(); } }; MegaDataEvent.prototype.stopPropagation = function() { this.isPropagationStopped = returnTrue; if (this.originalEvent) { this.originalEvent.stopPropagation(); } }; // Very simple replacement for jQuery.event function MegaDataEmitter() { /* dummy */ } inherits(MegaDataEmitter, null); _defineValue(MegaDataEmitter, 'seen', Object.create(null)); _defineValue(MegaDataEmitter, 'expando', '__event_emitter_' + (Math.random() * Math.pow(2, 56) - 1)); /** @function MegaDataEmitter.getEmitter */ _defineValue(MegaDataEmitter, 'getEmitter', function(event, target) { var emitter = target[MegaDataEmitter.expando]; if (!emitter) { emitter = Object.create(null); _defineValue(target, MegaDataEmitter.expando, emitter); } var pos; var src = event.type && event; var types = String(event.type || event).split(/\s+/).filter(String); var namespaces = Array(types.length); for (var i = types.length; i--;) { namespaces[i] = ''; if ((pos = types[i].indexOf('.')) >= 0) { namespaces[i] = types[i].substr(pos + 1).split('.').sort().join('.'); types[i] = types[i].substr(0, pos); } } return {types: types, namespaces: namespaces, event: src || types[0], events: emitter}; }); /** @function MegaDataEmitter.wrapOne */ _defineValue(MegaDataEmitter, 'wrapOne', function(handler) { return function _one(event) { this.off(event, _one); return handler.apply(this, arguments); }; }); MegaDataEmitter.prototype.off = function(event, handler) { if (event instanceof MegaDataEvent) { event.currentTarget.off(event.type + (event.namespace ? '.' + event.namespace : ''), handler); return this; } var emitter = MegaDataEmitter.getEmitter(event, this); for (var j = emitter.types.length; j--;) { var type = emitter.types[j]; var namespace = emitter.namespaces[j]; var handlers = emitter.events[type] || []; for (var i = handlers.length; i--;) { var tmp = handlers[i]; if (type === tmp.type && (!handler || handler.pid === tmp.pid) && (!namespace || namespace === tmp.namespace)) { handlers.splice(i, 1); } } if (!handlers.length) { delete emitter.events[type]; } } return this; }; MegaDataEmitter.prototype.one = function(event, handler, data) { return this.on(event, handler, data, true); }; MegaDataEmitter.prototype.on = function(event, handler, data, one) { var emitter = MegaDataEmitter.getEmitter(event, this); var events = emitter.events; handler = one ? MegaDataEmitter.wrapOne(handler) : handler; if (!handler.pid) { handler.pid = ++dsIncID; } for (var i = emitter.types.length; i--;) { var type = emitter.types[i]; var namespace = emitter.namespaces[i]; if (!events[type]) { events[type] = []; } events[type].push({ type: type, data: data, pid: handler.pid, handler: handler, namespace: namespace }); if (d) { MegaDataEmitter.seen[type] = 1; } } return this; }; // eslint-disable-next-line complexity MegaDataEmitter.prototype.trigger = function(event, data) { var emitter = MegaDataEmitter.getEmitter(event, this); event = new MegaDataEvent(emitter.event, this); event.data = data; // @todo require all trigger() calls to provide an array to prevent checking for isArray() data = data ? Array.isArray(data) ? data : [data] : []; var idx = data.length; var tmp = new Array(idx + 1); while (idx) { tmp[idx--] = data[idx]; } tmp[0] = event; data = tmp; var res; var type = emitter.types[0]; var namespace = emitter.namespaces[0]; var handlers = [].concat(emitter.events[type] || []); while ((tmp = handlers[idx++]) && !event.isPropagationStopped()) { event.currentTarget = this; event.namespace = namespace; if ((!namespace || namespace === tmp.namespace) && (res = tmp.handler.apply(this, data)) !== undefined) { event.result = res; if (res === false) { event.preventDefault(); event.stopPropagation(); } } } if (!event.isDefaultPrevented()) { tmp = this['on' + type]; if (typeof tmp === 'function') { event.result = tmp.apply(this, data); if (event.result === false) { event.preventDefault(); } } } if (event.originalEvent && event.result !== undefined) { event.originalEvent.returnValue = event.result; } return event.result; }; MegaDataEmitter.prototype.rebind = function(event, handler) { return this.off(event).on(event, handler); }; MegaDataEmitter.prototype.bind = MegaDataEmitter.prototype.on; MegaDataEmitter.prototype.unbind = MegaDataEmitter.prototype.off; Object.freeze(MegaDataEmitter); /** * Simple map-like implementation that tracks changes * * @param [parent] * @param [defaultData] * @constructor */ function MegaDataMap(parent, defaultData) { // MegaDataEmitter.call(this); /** @property MegaDataMap._parent */ _defineNonEnum(this, '_parent', parent || false); /** @property MegaDataMap._dataChangeIndex */ _defineNonEnum(this, '_dataChangeIndex', 0); /** @property MegaDataMap._dataChangeListeners */ _defineNonEnum(this, '_dataChangeListeners', []); /** @property MegaDataMap._dataChangeTrackedId */ _defineNonEnum(this, '_dataChangeTrackedId', ++dsIncID); /** @property MegaDataMap._data */ _defineNonEnum(this, '_data', defaultData || {}); Object.setPrototypeOf(this._data, null); if (d > 1) { if (!MegaDataMap.__instancesOf) { MegaDataMap.__instancesOf = new WeakMap(); } MegaDataMap.__instancesOf.set(this, Object.getPrototypeOf(this)); } } inherits(MegaDataMap, MegaDataEmitter); /** @property MegaDataMap.__ident_0 */ lazy(MegaDataMap.prototype, '__ident_0', function() { return this.constructor.name + '.' + ++dsIncID; }); /** @function MegaDataMap.prototype._schedule */ lazy(MegaDataMap.prototype, '_schedule', function() { var task = null; var self = this; var callTask = function _callTask() { if (task) { queueMicrotask(task); task = null; } }; return function _scheduler(callback) { if (!task) { queueMicrotask(callTask); } task = function _task() { callback.call(self); }; }; }); Object.defineProperty(MegaDataMap.prototype, 'length', { get: function() { return Object.keys(this._data).length; }, configurable: true }); _defineValue(MegaDataMap.prototype, 'valueOf', function() { return this.__ident_0; }); MegaDataMap.prototype.trackDataChange = function() { var idx = arguments.length; var args = new Array(idx); while (idx--) { args[idx] = arguments[idx]; } var self = this; this._schedule(function _trackDataChange() { var that = self; do { args.unshift(that); if (that === self) { that._dispatchChangeListeners(args); } else { that._enqueueChangeListenersDsp(args); } } while ((that = that._parent) instanceof MegaDataMap); }); }; MegaDataMap.prototype.addChangeListener = function(cb) { if (d) { var h = this._dataChangeListeners; if (d > 1 && h.length > 200) { _warnOnce(this, '%s: Too many handlers added(%d)! race?', this, h.length, [this]); } console.assert(h.indexOf(cb) < 0, 'handler exists'); if (typeof cb === 'function') { console.assert(!cb.__mdmChangeListenerID, 'reusing handler'); } else { console.assert(typeof cb.handleChangeEvent === 'function', 'invalid instance'); } } if (typeof cb === 'function') { /** @property Function.__mdmChangeListenerID */ _defineValue(cb, '__mdmChangeListenerID', dsIncID + 1); } this._dataChangeListeners.push(cb); return ++dsIncID; }; MegaDataMap.prototype.removeEventHandler = function(handler) { var result = false; var listeners = this._dataChangeListeners; if (d) { console.assert(handler && typeof handler.handleChangeEvent === 'function'); } for (var i = listeners.length; i--;) { if (listeners[i] === handler) { listeners.splice(i, 1); ++result; } } return result; }; MegaDataMap.prototype.removeChangeListener = function(cb) { var cId = cb && cb.__mdmChangeListenerID || cb; if (d) { console.assert(cId > 0, 'invalid listener id'); } if (cId > 0) { var listeners = this._dataChangeListeners; for (var i = listeners.length; i--;) { if (listeners[i].__mdmChangeListenerID === cId) { _defineValue(listeners[i], '__mdmChangeListenerID', 'nop'); listeners.splice(i, 1); if (d > 1) { while (--i > 0) { console.assert(listeners[i].__mdmChangeListenerID !== cId); } } return true; } } } return false; }; MegaDataMap.prototype._enqueueChangeListenersDsp = function(args) { var self = this; delay('mdm:cl:q.' + this.__ident_0, function() { self._dispatchChangeListeners(args); }, 40); }; MegaDataMap.prototype._dispatchChangeListeners = function(args) { var listeners = this._dataChangeListeners; if (d > 1) { console.debug('%s: dispatching %s awaiting listeners', this, listeners.length, [this]); } this._dataChangeIndex++; for (var i = listeners.length; i--;) { var result; var listener = listeners[i]; if (typeof listener === 'function') { result = listener.apply(this, args); } else if (listener) { result = listener.handleChangeEvent.apply(listener, args); } if (result === 0xDEAD) { this.removeChangeListener(listener); } } }; // eslint-disable-next-line local-rules/misc-warnings MegaDataMap.prototype.forEach = function(cb) { // this._data is a dict, so no guard-for-in needed // eslint-disable-next-line guard-for-in for (var k in this._data) { if (cb(this._data[k], k) === false) { break; } } }; MegaDataMap.prototype.every = function(cb) { var self = this; return self.keys().every(function(k) { return cb(self._data[k], k); }); }; MegaDataMap.prototype.some = function(cb) { var self = this; return self.keys().some(function(k) { return cb(self._data[k], k); }); }; MegaDataMap.prototype.map = function(cb) { var self = this; var res = []; self.forEach(function(v, k) { var intermediateResult = cb(v, k); if (intermediateResult !== null && intermediateResult !== undefined) { res.push(intermediateResult); } }); return res; }; MegaDataMap.prototype.keys = function() { return Object.keys(this._data); }; MegaDataMap.prototype.size = function() { return this.keys().length; }; MegaDataMap.prototype.destroy = tryCatch(function() { var self = this; Object.keys(self).map(function(k) { return self._removeDefinedProperty(k); }); Object.freeze(this); }); MegaDataMap.prototype.setObservable = function(k, defaultValue) { Object.defineProperty(this, k, { get: function() { return this.get(k, defaultValue); }, set: function(value) { this.set(k, value, false, defaultValue); }, enumerable: true }); }; MegaDataMap.prototype.exists = function(keyValue) { return keyValue in this._data; }; MegaDataMap.prototype.set = function(k, v, ignoreTrackDataChange) { if (d) { console.assert(k !== undefined && k !== false, "missing key"); } if (v instanceof MegaDataMap && !v._parent) { _defineNonEnum(v, '_parent', this); } if (_cmp(this._data[k], v) === true) { return; } this._data[k] = v; if (k in this) { this[k] = v; } else { Object.defineProperty(this, k, { get: function() { return this._data[k]; }, set: function(value) { if (value !== this._data[k]) { this._data[k] = value; this.trackDataChange(this._data, k, v); } }, enumerable: true, configurable: true }); } if (!ignoreTrackDataChange) { this.trackDataChange(this._data, k, v); } }; MegaDataMap.prototype.remove = function(k) { var v = this._data[k]; if (v instanceof MegaDataMap && v._parent === this) { _defineNonEnum(v, '_parent', null); } this._removeDefinedProperty(k); this.trackDataChange(this._data, k, v); }; /** @function MegaDataMap.prototype._removeDefinedProperty */ _defineValue(MegaDataMap.prototype, '_removeDefinedProperty', function(k) { if (k in this) { Object.defineProperty(this, k, { writable: true, value: undefined, configurable: true }); delete this[k]; } if (k in this._data) { delete this._data[k]; } }); /** @function MegaDataMap.prototype.toJS */ _defineValue(MegaDataMap.prototype, 'toJS', function() { return this._data; }); _defineValue(MegaDataMap.prototype, 'hasOwnProperty', function(prop) { return prop in this._data; }); _defineValue(MegaDataMap.prototype, 'propertyIsEnumerable', function(prop) { return this.hasOwnProperty(prop); }); /** * Plain Object-like container for storing data, with the following features: * - track changes ONLY on predefined list of properties * * @param {Object} [trackProperties] properties to observe for changes * @param {Object} [defaultData] default/initial data * @constructor */ function MegaDataObject(trackProperties, defaultData) { MegaDataMap.call(this, null, defaultData); if (trackProperties) { for (var k in trackProperties) { if (Object.hasOwnProperty.call(trackProperties, k)) { this.setObservable(k, trackProperties[k]); } } } /* if (d && typeof Proxy === 'function') { var slave = Object.create(Object.getPrototypeOf(this)); Object.setPrototypeOf(this, new Proxy(slave, { defineProperty: function(target, property, descriptor) { if (String(property).startsWith('jQuery')) { debugger console.assert(false); } Object.defineProperty(target, property, descriptor); return true; } })); }*/ } inherits(MegaDataObject, MegaDataMap); MegaDataObject.prototype.set = function(k, v, ignoreDataChange, defaultVal) { var notSet = !(k in this._data); if (notSet || _cmp(this._data[k], v) !== true) { if (notSet && _cmp(defaultVal, v) === true) { // this._data[...] is empty and defaultVal == newVal, DON'T track updates. return false; } if (!ignoreDataChange) { this.trackDataChange(this._data, k, v); } this._data[k] = v; } }; MegaDataObject.prototype.get = function(k, defaultVal) { return this._data && k in this._data ? this._data[k] : defaultVal; }; /** * MegaDataSortedMap * @param keyField * @param sortField * @param parent * @constructor */ function MegaDataSortedMap(keyField, sortField, parent) { MegaDataMap.call(this, parent); /** @property MegaDataSortedMap._parent */ _defineNonEnum(this, '_parent', parent || false); /** @property MegaDataSortedMap._sortedVals */ _defineNonEnum(this, '_sortedVals', []); /** @property MegaDataSortedMap._keyField */ _defineNonEnum(this, '_keyField', keyField); /** @property MegaDataSortedMap._sortField */ _defineNonEnum(this, '_sortField', sortField); } inherits(MegaDataSortedMap, MegaDataMap); Object.defineProperty(MegaDataSortedMap.prototype, 'length', { get: function() { return this._sortedVals.length; }, configurable: true }); // eslint-disable-next-line local-rules/misc-warnings MegaDataSortedMap.prototype.forEach = function(cb) { for (var i = 0; i < this._sortedVals.length; ++i) { var k = this._sortedVals[i]; cb(this._data[k], k); } }; MegaDataSortedMap.prototype.replace = function(k, newValue) { if (this._data[k] === newValue) { // already the same, save some CPU and do nothing. return true; } if (k in this._data) { // cleanup if (newValue[this._keyField] !== k) { this.removeByKey(k); } this.push(newValue); return true; } return false; }; /** @property MegaDataSortedMap._comparator */ lazy(MegaDataSortedMap.prototype, '_comparator', function() { var self = this; if (this._sortField === undefined) { return indexedDB.cmp.bind(indexedDB); } if (typeof self._sortField === "function") { return function(a, b) { return self._sortField(self._data[a], self._data[b]); }; } return function(a, b) { var sortFields = self._sortField.split(","); for (var i = 0; i < sortFields.length; i++) { var sortField = sortFields[i]; var ascOrDesc = 1; if (sortField[0] === '-') { ascOrDesc = -1; sortField = sortField.substr(1); } var _a = self._data[a][sortField]; var _b = self._data[b][sortField]; if (_a !== undefined && _b !== undefined) { if (_a < _b) { return -1 * ascOrDesc; } if (_a > _b) { return ascOrDesc; } return 0; } } return 0; }; }); MegaDataSortedMap.prototype.push = function(v) { var self = this; var keyVal = v[self._keyField]; if (keyVal in self._data) { self.removeByKey(keyVal); } self.set(keyVal, v, true); var minIndex = 0; var maxIndex = this._sortedVals.length - 1; var currentIndex; var currentElement; var cmp = self._comparator; var result = false; while (minIndex <= maxIndex) { currentIndex = (minIndex + maxIndex) / 2 | 0; currentElement = this._sortedVals[currentIndex]; var cmpResult = cmp(currentElement, keyVal); if (cmpResult === -1) { minIndex = currentIndex + 1; } else if (cmpResult === 1) { maxIndex = currentIndex - 1; } else { result = true; break; } } if (!result) { if (currentElement === undefined) { // first self._sortedVals.push(keyVal); } else { self._sortedVals.splice(cmp(currentElement, keyVal) === -1 ? currentIndex + 1 : currentIndex, 0, keyVal); } self.trackDataChange(); } else { // found another item in the list, with the same order value, insert after self._sortedVals.splice(currentIndex, 0, keyVal); } return self._sortedVals.length; }; MegaDataSortedMap.prototype.removeByKey = MegaDataSortedMap.prototype.remove = function(keyValue) { if (keyValue in this._data) { array.remove(this._sortedVals, keyValue); this._removeDefinedProperty(keyValue); this.trackDataChange(); return true; } return false; }; MegaDataSortedMap.prototype.exists = function(keyValue) { return keyValue in this._data; }; MegaDataSortedMap.prototype.keys = function() { return this._sortedVals; }; MegaDataSortedMap.prototype.values = function() { var res = []; // eslint-disable-next-line local-rules/misc-warnings this.forEach(function(v) { res.push(v); }); return res; }; MegaDataSortedMap.prototype.getItem = function(num) { return this._data[this._sortedVals[num]]; }; MegaDataSortedMap.prototype.indexOfKey = function(value) { return this._sortedVals.indexOf(value); }; MegaDataSortedMap.prototype.clear = function() { _defineNonEnum(this, '_sortedVals', []); _defineNonEnum(this, '_data', Object.create(null)); if (this.trackDataChange) { this.trackDataChange(); } }; /** * Simplified version of `Array.prototype.splice`, only supports 2 args (no adding/replacement of items) for now. * * @param {Number} start first index to start from * @param {Number} deleteCount number of items to delete * @returns {Array} array of deleted item ids */ MegaDataSortedMap.prototype.splice = function(start, deleteCount) { var deletedItemIds = this._sortedVals.splice(start, deleteCount); for (var i = deletedItemIds.length; i--;) { this._removeDefinedProperty(deletedItemIds[i]); } this.trackDataChange(); return deletedItemIds; }; /** * Returns a regular array (not a sorted map!) of values sliced as with `Array.prototype.slice` * * @param {Number} begin first index to start from * @param {Number} end last index where to end the "slice" * @returns {Array} array of removed IDs */ MegaDataSortedMap.prototype.slice = function(begin, end) { var results = this._sortedVals.slice(begin, end); for (var i = results.length; i--;) { results[i] = this._data[results[i]]; } return results; }; var testMegaDataSortedMap = function() { var arr1 = new MegaDataSortedMap("id", "orderValue,ts"); arr1.push({ 'id': 1, 'ts': 1, 'orderValue': 1 }); arr1.push({ 'id': 2, 'ts': 3, 'orderValue': 2 }); arr1.push({ 'id': 3, 'ts': 2 }); arr1.forEach(function(v, k) { console.error(v, k); }); return arr1; }; /** * Generic "MegaDataBitMap" manager that manages a list of all registered (by unique name) MegaDataBitMaps * * @constructor */ function MegaDataBitMapManager() { this._bitmaps = Object.create(null); } inherits(MegaDataBitMapManager, null); /** * Register a MegaDataBitMap * @param {String} name * @param {MegaDataBitMap} megaDataBitMap */ MegaDataBitMapManager.prototype.register = function(name, megaDataBitMap) { if (this._bitmaps[name] !== undefined) { console.error("Tried to register a MegaDataBitMap that already exists (at least with that name)."); return; } this._bitmaps[name] = megaDataBitMap; }; /** * Check if an MegaDataBitMap with a specific `name` exists. * * @param {String} name * @returns {Boolean} */ MegaDataBitMapManager.prototype.exists = function(name) { return typeof(this._bitmaps[name]) !== 'undefined'; }; /** * Get the instance of a specific by `name` MegaDataBitMap * * @param {String} name * @returns {*} */ MegaDataBitMapManager.prototype.get = function(name) { return this._bitmaps[name]; }; /** * MegaDataBitMaps are array, that are stored as attributes (on the MEGA API side), which hold 0s and 1s for a specific * (predefined, ordered set of) keys. * Once the data is .commit()'ed adding new keys should always be done at the end of the array. * No keys should be removed, because that would mess up the values stored in the user attribute, since all keys are * only available on the client side, the data is mapped via the key index (e.g. key1 = 0, key2 = 1, keyN = N - 1). * * @param {String} name Should be unique. * @param {Boolean} isPub should the attribute be public or private? * @param {Array} keys Array of keys * @constructor */ function MegaDataBitMap(name, isPub, keys) { var self = this; MegaDataObject.call(self, array.to.object(keys, 0)); self.name = name; self._keys = keys; self._isPub = isPub; self._data = new Uint8Array(keys.length); self._updatedMask = new Uint8Array(keys.length); self._version = null; self._readyPromise = new MegaPromise(); attribCache.bitMapsManager.register(name, self); mega.attr.get(u_handle, name, self.isPublic() ? true : -2, true) .then(function(r) { if (typeof r !== 'string') { throw r; } self.mergeFrom(r, false); }) .catch(function(ex) { if (ex !== -9) { // -9 is ok, means the attribute does not exists on the server console.error("mega.attr.get failed:", ex); } }) .always(SoonFc(function() { self._readyPromise.resolve(); })); } inherits(MegaDataBitMap, MegaDataObject); Object.defineProperty(MegaDataBitMap.prototype, 'length', { get: function() { return this._data.length; }, enumerable: false, configurable: true }); /** * Returns a list of keys that are currently registered with this MegaDataBitMap instance. * * @returns {Array} */ MegaDataBitMap.prototype.keys = function() { return this._keys; }; /** * Flip the value of `key` from 0 -> 1 or from 1 -> 0 * Calling this function would trigger a change event. * Calling this function would NOT persist the data on the server, until the .commit() method is called. * * @param {String} key * @returns {Boolean} */ MegaDataBitMap.prototype.toggle = function(key) { var keyIdx = this._keys.indexOf(key); if (keyIdx === -1) { return false; } this.set(key, !this._data[keyIdx] ? 1 : 0); }; /** * Reset the internal "updated mask" to mark all keys as commited. * Mainly used internally by `MegaDataBitMap.prototype.commit()` */ MegaDataBitMap.prototype.commited = function() { this._updatedMask = new Uint8Array(this._keys.length); }; /** * Change the value of `key` to `v` (can be either 0 or 1, integer). * Calling this function would trigger a change event. * Calling this function would NOT persist the data on the server, until the .commit() method is called. * * @param {String} key * @param {Number} v Can be either 0 or 1 * @param {Boolean} ignoreDataChange If true, would not trigger a change event * @param {Number} defaultVal By default, the default value is supposed to be 0, but any other value can be passed here */ MegaDataBitMap.prototype.set = function(key, v, ignoreDataChange, defaultVal) { if (typeof(v) !== 'number' && v !== 1 && v !== 0) { console.error("MegaDataBitMap...set was called with non-zero/one value as 2nd argument."); return; } var self = this; self._readyPromise.done(function() { defaultVal = defaultVal ? defaultVal : 0; var keyIdx = self._keys.indexOf(key); if (keyIdx === -1) { return false; } if ( ( typeof(self._data[keyIdx]) === 'undefined' && typeof(defaultVal) !== 'undefined' && _cmp(defaultVal, v) === true ) || ( self._data[keyIdx] === v /* already the same value... */ ) ) { // self._data[...] is empty and defaultVal == newVal, DON'T track updates. return false; } self._data[keyIdx] = v; self._updatedMask[keyIdx] = 1; if (!ignoreDataChange) { self.trackDataChange(self._data, key, v); } }); }; /** * Optionally check if the MegaDataBitMap is ready to be used, e.g. the data is retrieved from the server. * .set and .get are automatically going to wait for the data to be loaded from the API, so this is not needed to be * called before .get/.set, but in all other use cases this can be used. * * @returns {Boolean} */ MegaDataBitMap.prototype.isReady = function() { return this._readyPromise.state() !== 'pending'; }; MegaDataBitMap.prototype.get = function(key, defaultVal) { var self = this; var resPromise = new MegaPromise(); self._readyPromise .done(function() { defaultVal = defaultVal ? defaultVal : false; var keyIdx = self._keys.indexOf(key); if (keyIdx === -1) { resPromise.reject(key); return undefined; } resPromise.resolve( self._data && typeof(self._data[keyIdx]) !== 'undefined' ? self._data[keyIdx] : defaultVal ); }) .fail(function() { resPromise.reject(arguments); }); return resPromise; }; /** * Merge the current MegaDataBitMap value with a {String} `str`. * Merging is done the following way: * - IF a value of a key, passed by `str` differs from the one in the current instance: * a) if was marked as 'dirty' (not commited, via the update mask) it would not be merged (its assumed that any data, * stored in 'dirty' state and not commited is the most up to date one) * b) the local value for that key would be updated, following a change event * * @param {String} str String, containing 0 and 1 chars to be parsed as Uint8Array with 0 and 1s * @param {Boolean} requiresCommit Pass true, to mark all changes in the update mask (e.g. they would be schedulled for * sending to the server on the next .commit() call) */ MegaDataBitMap.prototype.mergeFrom = function(str, requiresCommit) { var self = this; var targetLength = str.length; if (self._keys.length > str.length) { targetLength = self._keys.length; } for (var i = 0, strLen = str.length; i < strLen; i++) { var newVal = str.charCodeAt(i); if (self._data[i] !== newVal) { if (self._updatedMask[i] && self._updatedMask[i] === 1) { // found uncommited change, would leave (and not merge), since in that case, we would assume that // since changes are commited (almost) immediately after the .set()/.toggle() is done, then this // was just changed and its newer/up to date then the one from the server. } else { self._data[i] = newVal; self.trackDataChange( self._data, self._keys[i], newVal ); if (requiresCommit) { self._updatedMask[i] = 1; } } } } // resize if needed. if (self._keys.length > targetLength) { self._data.fill(false, self._keys.length, targetLength - self._keys.length); } }; /** * Convert to a base64urlencoded string * * @returns {String} */ _defineValue(MegaDataBitMap.prototype, 'toString', function() { return base64urlencode(String.fromCharCode.apply(null, this._data)); }); /** * Convert the mask to a base64urlencoded string * * @returns {String} */ MegaDataBitMap.prototype.maskToString = function() { return base64urlencode( String.fromCharCode.apply(null, this._updatedMask) ); }; /** * Convert to a 0 and 1 string (separated by ",") * * @returns {String} */ MegaDataBitMap.prototype.toDebugString = function() { return this._data.toString(); }; /** * Set the current version of the attribute (received and controlled by the API) * * @param ver * @returns {String} */ MegaDataBitMap.prototype.setVersion = function(ver) { return this._version = ver; }; /** * Get the current version of the attribute (received and controlled by the API) * * @returns {String|undefined} */ MegaDataBitMap.prototype.getVersion = function() { return this._version; }; /** * Was this attribute marked as public? * * @returns {Boolean} */ MegaDataBitMap.prototype.isPublic = function() { return this._isPub; }; /** * Commits all changes which were marked as changed. * All changed keys/bits would be overwritten on the server * All non-changed keys/bits, may be altered in case another commit (by another client) had changed them. In that case, * a change event would be triggered. * * @returns {MegaPromise} */ MegaDataBitMap.prototype.commit = function() { var self = this; var masterPromise = new MegaPromise(); if (self._commitTimer) { clearTimeout(self._commitTimer); } self._commitTimer = setTimeout(function() { if (self._commitPromise) { // commit is already in progress, create a proxy promise that would execute after the current commit op and // return it self._commitPromise.always(function () { masterPromise.linkDoneAndFailTo(self.commit()); }); return; } self._commitPromise = new MegaPromise(); masterPromise.linkDoneAndFailTo(self._commitPromise); self._commitPromise.always(function () { delete self._commitPromise; }); // check if we really need to commit anything (e.g. mask is not full of zeroes) var foundOnes = false; for (var i = 0; i < self._updatedMask.length; i++) { if (self._updatedMask[i] === 1) { foundOnes = true; break; } } // no need to commit anything. if (foundOnes === false) { var commitPromise = self._commitPromise; self._commitPromise.resolve(false); return; } var attributeFullName = (self.isPublic() ? "+!" : "^!") + self.name; var cacheKey = u_handle + "_" + attributeFullName; attribCache.setItem(cacheKey, JSON.stringify([self.toString(), 0])); api_req( { "a": "usma", "n": attributeFullName, "ua": self.toString(), "m": self.maskToString() }, { callback: function megaDataBitMapCommitCalback(response) { if (typeof(response) === 'number') { self._commitPromise.reject(response); } else { if (response.ua && response.ua !== self.toString()) { self.mergeFrom(base64urldecode(response.ua)); attribCache.setItem(cacheKey, JSON.stringify([self.toString(), 0])); } if (response.v) { self.setVersion(response.v); } self.commited(); self._commitPromise.resolve(response); } } } ); }, 100); return masterPromise; }; /** * Initialise a new MegaDataBitMap from string * Only used for testing some stuff. * * @param {String} name * @param {Boolean} isPub * @param {Array} keys * @param {String} base64str * @param {*} [parent] * @returns {MegaDataBitMap} */ MegaDataBitMap.fromString = function(name, isPub, keys, base64str, parent) { var str = base64urldecode(base64str); var targetLength = str.length; if (keys.length > str.length) { targetLength = keys.length; } var buf = new ArrayBuffer(targetLength); // 2 bytes for each char var bufView = new Uint8Array(buf); for (var i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } var mdbm = new MegaDataBitMap(name, isPub, keys, parent); mdbm._data = new Uint8Array(buf, 0, buf.byteLength); if (keys.length > buf.length) { mdbm._data.fill(false, keys.length, buf.length - keys.length); } return mdbm; }; /** * Mark all bits/keys as 0s (would not commit the changes). */ MegaDataBitMap.prototype.reset = function() { var self = this; self.keys().forEach(function(k) { self.set(k, 0); }); }; /** * Experiments, tests and examples * * @returns {MegaDataBitMap} */ var testMegaDataBitMap = function() { var keys = [ 'key1', 'key2', 'key3', ]; var arr1 = new MegaDataBitMap("arr1", false, keys); arr1.toggle('key2'); arr1.commited(); var arr2 = MegaDataBitMap.fromString("arr2", false, keys, arr1.toString()); assert(arr2.toString() === arr1.toString()); console.error(arr2._updatedMask.toString()); arr2.toggle('key1'); console.error(arr2._updatedMask.toString()); arr1.mergeFrom(arr2.toString()); return arr1; }; /** * Bitmap based on an integer. * @param attribute {String} * Name of the attribute. * @param map An array of keys to use for identifying each bit. * @param pub {Boolean|Number} * True for public attributes (default: true). * -1 for "system" attributes (e.g. without prefix) * -2 for "private non encrypted attributes" * False for private encrypted attributes * @param nonHistoric {Boolean} * True for non-historic attributes (default: false). Non-historic attributes will overwrite the value, and * not retain previous values on the API server. * @param autoSaveTimeout {int} Autosave after x millisecond. * @constructor */ function MegaIntBitMap(attribute, map, pub, nonHistoric, autoSaveTimeout) { this.value = undefined; this.attribute = attribute; this.map = map; this.pub = pub; this.nonHistoric = nonHistoric; this.isReadyPromise = null; this.autoSaveTimeout = autoSaveTimeout; this.autoSaveTimer = null; } /** * Get a bit based on its key. * @param key The bit key. * @returns {MegaPromise} */ MegaIntBitMap.prototype.get = function(key) { var self = this; return new MegaPromise(function(resolve, reject) { self.isReady().then(function() { var mask; if (Array.isArray(key)) { var bitKey; var result = {}; for (var i = 0; i < key.length; i++) { bitKey = key[i]; mask = self.getMask(bitKey); if (!mask) { reject("Invalid Key"); return false; } result[bitKey] = self.value & mask ? true : false; } resolve(result); } else { mask = self.getMask(key); if (!mask) { reject("Invalid Key"); return false; } resolve(self.value & mask ? true : false); } }, reject); }); }; /** * Set a bit/bits based on a key/keys. * @param key object|string The bit key or map of bit keys -> newState * @param newValue {bool|void} The new state if previous parameter is a bit key. * @returns {MegaPromise} */ MegaIntBitMap.prototype.set = function(key, newValue) { var self = this; return new MegaPromise(function(resolve, reject) { self.isReady().then(function() { var mask; // jscs:disable disallowImplicitTypeConversion if (typeof key === 'object') { var bitKey; var updatedValue = self.value; var keys = Object.keys(key); for (var i = 0; i < keys.length; i++) { bitKey = keys[i]; mask = self.getMask(bitKey); if (!mask) { reject("Invalid Key"); return false; } updatedValue = key[bitKey] ? (updatedValue | mask) : (updatedValue & (~mask)); } self.value = updatedValue; } else { mask = self.getMask(key); if (!mask) { reject("Invalid Key"); return false; } self.value = newValue ? (self.value | mask) : (self.value & (~mask)); } // jscs:enable disallowImplicitTypeConversion self.valueChanged(); resolve(self.value); }, reject); }); }; /** * Get all bits. * @returns {MegaPromise} */ MegaIntBitMap.prototype.getAll = function() { var self = this; return new MegaPromise(function(resolve, reject) { self.isReady().then(function() { var all = {}; for (var i = 0; i < self.map.length; i++) { all[self.map[i]] = self.value & (1 << i) ? true : false; } resolve(all); }, reject); }); }; /** * Set all bits that we know about. * @param newValue The new state for all known bits. * @returns {MegaPromise} */ MegaIntBitMap.prototype.setAll = function(newValue) { var self = this; return new MegaPromise(function(resolve, reject) { self.isReady().then(function() { // jscs:disable disallowImplicitTypeConversion var mask = ~(0xFFFFFF << self.map.length); self.value = newValue ? self.value | mask : self.value & (~mask); // jscs:enable disallowImplicitTypeConversion self.valueChanged(); resolve(self.value); }, reject); }); }; /** * Get a mask from a key. * @param key The bit key. */ MegaIntBitMap.prototype.getMask = function(key) { var idx = this.map.indexOf(key); if (idx >= 0) { return 1 << idx; } return false; }; /** * Load attribute. * @returns {MegaPromise} */ MegaIntBitMap.prototype.load = function() { var self = this; return new MegaPromise(function(resolve, reject) { mega.attr.get(u_attr.u, self.attribute, self.pub, self.nonHistoric).then(function(value) { self.value = parseInt(value); resolve(); }, function(value) { if (value === ENOENT) { self.value = 0; resolve(); } else { reject.apply(null, arguments); } }); }); }; /** * Save Attribute. * @returns {MegaPromise} */ MegaIntBitMap.prototype.save = function() { return mega.attr.set( this.attribute, this.value, this.pub, this.nonHistoric ); }; /** * Wait till ready. * @returns {MegaPromise} */ MegaIntBitMap.prototype.isReady = function() { if (this.isReadyPromise === null) { var self = this; this.isReadyPromise = new MegaPromise(function(resolve, reject) { self.load().then(resolve, reject); }); } return this.isReadyPromise; }; /** * Directly set all the bits by providing an int. * @param newValue {int} The new value * @returns {MegaPromise} */ MegaIntBitMap.prototype.setValue = function(newValue) { var self = this; return new MegaPromise(function(resolve, reject) { self.isReady().then(function() { self.value = newValue; self.valueChanged(); resolve(self.value); }, reject); }); }; /** * Track value changed. * Note: Call this whenever the value is changed. */ MegaIntBitMap.prototype.valueChanged = function() { if (this.autoSaveTimeout) { var self = this; clearTimeout(this.autoSaveTimer); this.autoSaveTimer = setTimeout(function() { clearTimeout(self.autoSaveTimer); self.save(); }, self.autoSaveTimeout); } }; /** * Triggered when the attribute is updated, thus updating our internal value. * @return {MegaPromise} */ MegaIntBitMap.prototype.handleAttributeUpdate = function() { this.isReadyPromise = null; return this.isReady(); }; // ---------------------------------------------------------------------------------------- Object.defineProperty(global, 'MegaDataMap', {value: MegaDataMap}); Object.defineProperty(global, 'MegaDataObject', {value: MegaDataObject}); Object.defineProperty(global, 'MegaDataSortedMap', {value: MegaDataSortedMap}); /** @constructor MegaDataEvent */ Object.defineProperty(global, 'MegaDataEvent', {value: MegaDataEvent}); /** @constructor MegaDataEmitter */ Object.defineProperty(global, 'MegaDataEmitter', {value: MegaDataEmitter}); Object.defineProperty(global, 'MegaIntBitMap', {value: MegaIntBitMap}); Object.defineProperty(global, 'MegaDataBitMap', {value: MegaDataBitMap}); Object.defineProperty(global, 'MegaDataBitMapManager', {value: MegaDataBitMapManager}); if (d) { if (d > 1) { _timing(MegaDataMap); _timing(MegaDataObject); _timing(MegaDataEmitter); _timing(MegaDataSortedMap); } global._timing = _timing; global.testMegaDataBitMap = testMegaDataBitMap; global.testMegaDataSortedMap = testMegaDataSortedMap; } })(self); /** * IndexedDB Key/Value Storage */ // (the name must exist in the FMDB schema with index 'k') function IndexedDBKVStorage(name) { 'use strict'; this.name = name; this.dbcache = Object.create(null); // items that reside in the DB this.newcache = Object.create(null); // new items that are pending flushing to the DB this.delcache = Object.create(null); // delete items that are pending deletion from the DB } IndexedDBKVStorage.prototype = Object.create(null); // sets fmdb reference and prefills the memory cache from the DB // (call this ONCE as soon as the user-specific IndexedDB is open) // (this is robust against an undefined fmdb reference) IndexedDBKVStorage.prototype.load = function() { 'use strict'; var self = this; return new MegaPromise(function(resolve) { if (!window.fmdb) { return resolve(); } fmdb.get(self.name) .always(function(r) { for (var i = r.length; i--;) { self.dbcache[r[i].k] = r[i].v; } resolve(); }); }); }; // flush new items / deletions to the DB (in channel 0, this should // be followed by call to setsn()) // will be a no-op if no fmdb set IndexedDBKVStorage.prototype.flush = function() { 'use strict'; var k; var fmdb = window.fmdb || false; for (k in this.delcache) { if (fmdb) { fmdb.del(this.name, k); } delete this.dbcache[k]; } for (k in this.newcache) { if (fmdb) { fmdb.add(this.name, {k: k, d: {v: this.newcache[k]}}); } this.dbcache[k] = this.newcache[k]; } this.delcache = Object.create(null); this.newcache = Object.create(null); }; // set item in DB/cache // (must only be called in response to an API response triggered by an actionpacket) IndexedDBKVStorage.prototype.setItem = function __IDBKVSetItem(k, v) { 'use strict'; var self = this; console.assert(v !== undefined); return new MegaPromise(function(resolve) { delete self.delcache[k]; self.newcache[k] = v; self.saveState(); resolve([k, v]); }); }; // get item - if not found, promise will be rejected IndexedDBKVStorage.prototype.getItem = function __IDBKVGetItem(k) { 'use strict'; var self = this; return new MegaPromise(function(resolve, reject) { if (!self.delcache[k]) { if (self.newcache[k] !== undefined) { // record recently (over)written return resolve(self.newcache[k]); } // record available in DB if (self.dbcache[k] !== undefined) { return resolve(self.dbcache[k]); } } // record deleted or unavailable reject(); }); }; // remove item from DB/cache // (must only be called in response to an API response triggered by an actionpacket) IndexedDBKVStorage.prototype.removeItem = function __IDBKVRemoveItem(k) { 'use strict'; this.delcache[k] = true; delete this.newcache[k]; this.saveState(); return MegaPromise.resolve(); }; // enqueue explicit flush IndexedDBKVStorage.prototype.saveState = function() { 'use strict'; var self = this; delay('attribcache:savestate', function() { if (d) { console.debug('attribcache:savestate(%s)...', currsn, fminitialized); } if (fminitialized && currsn) { if (window.fmdb) { setsn(currsn); } else { self.flush(); } } }, 2600); }; // Clear DB Table and in-memory contents. IndexedDBKVStorage.prototype.clear = promisify(function __IDBKVClear(resolve, reject) { 'use strict'; console.error("This function should not be used under normal conditions..."); IndexedDBKVStorage.call(this, this.name); if (window.fmdb && Object(fmdb.db).hasOwnProperty(this.name)) { return fmdb.db[this.name].clear().then(resolve).catch(reject); } reject(); }); if (!is_karma) { Object.freeze(IndexedDBKVStorage.prototype); } Object.freeze(IndexedDBKVStorage); var attribCache = false; mBroadcaster.once('boot_done', function() { 'use strict'; attribCache = new IndexedDBKVStorage('ua'); attribCache.bitMapsManager = new MegaDataBitMapManager(); // We no longer need this for anything else. window.IndexedDBKVStorage = null; }); /** * Shared, Local Key Value Storage. * To be used for storing local (non-api persisted data, mostly non-critical data). * * @param name {String} * @param manualFlush {bool} by default disabled, note: NOT tested/used yet. * @param [broadcaster] {Object} mBroadcaster-like object in case you don't want to use the global mBroadcaster * and watchdog (useful for unit tests - see test/utilities/fakebroadcaster.js) * * @constructor */ var SharedLocalKVStorage = function(name, manualFlush, broadcaster) { var self = this; if (!broadcaster) { broadcaster = mBroadcaster; } self.broadcaster = broadcaster; // intentionally using '.wdog' instead of '.watchdog', because a self.wdog (where 'self' is not defined), // would basically cause our code to use the global 'watchdog' object, which can cause a lot of hard to track // issues! if (typeof broadcaster.watchdog !== 'undefined') { self.wdog = broadcaster.watchdog; } else { self.wdog = watchdog; } self.name = name; self.manualFlush = manualFlush; var loggerOpts = {}; if (localStorage.SharedLocalKVStorageDebug) { loggerOpts['isEnabled'] = true; loggerOpts['minLogLevel'] = function () { return MegaLogger.LEVELS.DEBUG; }; loggerOpts['transport'] = function (level, args) { var fn = "log"; if (level === MegaLogger.LEVELS.DEBUG) { fn = "debug"; } else if (level === MegaLogger.LEVELS.LOG) { fn = "log"; } else if (level === MegaLogger.LEVELS.INFO) { fn = "info"; } else if (level === MegaLogger.LEVELS.WARN) { fn = "warn"; } else if (level === MegaLogger.LEVELS.ERROR) { fn = "error"; } else if (level === MegaLogger.LEVELS.CRITICAL) { fn = "error"; } args.push("[" + fn + "]"); console.warn.apply(console, args); }; } else { loggerOpts['isEnabled'] = true; loggerOpts['minLogLevel'] = function () { return MegaLogger.LEVELS.WARN; }; } self.logger = new MegaLogger("SharedLocalKVStorage[" + name + ":" + broadcaster.id + "]", loggerOpts); self.persistAdapter = null; self._queuedSetOperations = {}; self._listeners = {}; self._initPersistance(); Object.defineProperty(this, 'isMaster', { get: function() { return !!self.broadcaster.crossTab.master; }, set: function() { throw new Error(".isMaster is read only!"); } }); }; inherits(SharedLocalKVStorage, MegaDataEmitter); /** * Worst case scenario of an inactive tab, that is heavily throttled by Chrome, so we need to set the query time out * when running in realworld cases to a bit higher value. */ SharedLocalKVStorage.DEFAULT_QUERY_TIMEOUT = ( mega.chrome ? 10000 : 1000 ); SharedLocalKVStorage._replyToQuery = function(watchdog, token, query, value) { watchdog.notify('Q!Rep!y', { query: query, token: token, value: value }); }; SharedLocalKVStorage.prototype.triggerOnChange = function(k, v) { var self = this; self.trigger('onChange', [k, v]); }; SharedLocalKVStorage.prototype._setupPersistance = function() { var self = this; // clear any old/previously added event handlers in case this function is called after a master change [ 'watchdog:Q!slkv_get_' + self.name, 'watchdog:Q!slkv_keys_' + self.name, 'watchdog:Q!slkv_set_' + self.name, 'watchdog:slkv_mchanged_' + self.name, 'crossTab:master' ].forEach(function(k) { if (self._listeners[k]) { self.broadcaster.removeListener(self._listeners[k]); delete self._listeners[k]; } // self.wdog.removeEventHandler(k); }) ; var listenersMap = {}; if (self.broadcaster.crossTab.master) { // i'm the cross tab master self.persistAdapter = new SharedLocalKVStorage.Utils.DexieStorage( self.name, self.manualFlush, self.wdog.wdID ); listenersMap["watchdog:Q!slkv_keys_" + self.name] = function (args) { var token = args.data.reply; assert(token, 'token is missing for: ' + JSON.stringify(args)); self.keys(args.data.p).done(function (keys) { SharedLocalKVStorage._replyToQuery(self.wdog, token, "Q!slkv_keys_" + self.name, keys); }); }; listenersMap["watchdog:Q!slkv_get_" + self.name] = function(args) { var token = args.data.reply; self.getItem(args.data.k) .done(function(response) { self.logger.debug("Sending slkv_get reply: ", args.data.k, response); SharedLocalKVStorage._replyToQuery(self.wdog, token, "Q!slkv_get_" + self.name, response); }) .fail(function() { SharedLocalKVStorage._replyToQuery(self.wdog, token, "Q!slkv_get_" + self.name, undefined); }); }; listenersMap["watchdog:Q!slkv_set_" + self.name] = function(args) { var token = args.data.reply; var result; if (typeof args.data.v === 'undefined') { result = self.removeItem(args.data.k, { 'origin': args.origin }); } else { result = self.setItem(args.data.k, args.data.v, { 'origin': args.origin }); } result .done(function(response) { SharedLocalKVStorage._replyToQuery(self.wdog, token, "Q!slkv_set_" + self.name, response); }) .fail(function() { SharedLocalKVStorage._replyToQuery(self.wdog, token, "Q!slkv_set_" + self.name, undefined); }); }; } else { self.persistAdapter = false; listenersMap["watchdog:slkv_mchanged_" + self.name] = function(args) { if (args.data.meta.origin !== self.wdog.wdID) { self.triggerOnChange(args.data.k, args.data.v); } }; listenersMap['crossTab:master'] = function(args) { // .setMaster was locally called. self._setupPersistance(); }; } Object.keys(listenersMap).forEach(function(k) { self._listeners[k] = self.broadcaster.addListener(k, listenersMap[k]); }); }; SharedLocalKVStorage.prototype._initPersistance = function() { var self = this; self._setupPersistance(); if (d) { self.rebind("onChange.logger" + self.name, function(e, k, v) { self.logger.debug("Got onChange event:", k, v); }); } self._leavingListener = self.broadcaster.addListener('crossTab:leaving', function slkv_crosstab_leaving(data) { // master had changed? if (data.data.wasMaster) { self._setupPersistance(); if (data.data.newMaster !== -1) { if (data.data.newMaster === self.broadcaster.crossTab.ctID) { self.logger.debug("I'd been elected as master."); } else { self.logger.debug("New master found", data.data.newMaster); } // master had changed, do I've any queued ops that were not executed? re-send them! Object.keys(self._queuedSetOperations).forEach(function(k) { var ops = self._queuedSetOperations[k]; ops.forEach(function(op) { if (op.state && op.state() === 'pending') { self.setItem(k, op.targetValue) .done(function() { op.resolve(); }) .fail(function() { op.reject(); }) .always(function() { var index = self._queuedSetOperations[k].indexOf(op); if (index > -1) { self._queuedSetOperations[k].splice(index, 1); } }); self.logger.debug("Re-setting value for", k, "to", op.targetValue, "because a new " + "master had been elected."); } }); }); } } }); }; SharedLocalKVStorage.prototype.getItem = function(k) { var self = this; if (self.broadcaster.crossTab.master) { return this.persistAdapter.getItem(k); } else { // request using cross tab from master var promise = new MegaPromise(); self.wdog.query("slkv_get_" + self.name, SharedLocalKVStorage.DEFAULT_QUERY_TIMEOUT, false, {'k': k}, true) .done(function(response) { if (response && response[0] && typeof response[0] !== 'undefined') { promise.resolve(response[0]); } else { promise.reject(response[0]); } }) .fail(function(e) { self.logger.warn("getItem request failed: ", k, e); promise.reject(e); }); return promise; } }; SharedLocalKVStorage.prototype.eachPrefixItem = function __SLKVEachItem(prefix, cb) { var self = this; if (self.broadcaster.crossTab.master) { return self.persistAdapter.eachPrefixItem(prefix, cb); } else { var masterPromise = new MegaPromise(); self.keys(prefix) .fail(function(err) { self.logger.warn("eachPrefixItem", prefix, "failed"); masterPromise.reject(err); }) .done(function(keys) { var promises = []; keys.forEach(function(k) { var promise = new MegaPromise(); promises.push(promise); self.getItem(k) .done(function(v) { cb(v, k); promise.resolve(); }) .fail(function(err) { self.logger.warn("eachPrefixItem -> getItem", prefix, "failed"); promise.reject(err); }); }); masterPromise.linkDoneAndFailTo(MegaPromise.allDone(promises)); }); return masterPromise; } }; SharedLocalKVStorage.prototype.keys = function(prefix) { var self = this; if (self.broadcaster.crossTab.master) { return self.persistAdapter.keys(prefix); } else { // request using cross tab from master var promise = new MegaPromise(); self.wdog.query( "slkv_keys_" + self.name, SharedLocalKVStorage.DEFAULT_QUERY_TIMEOUT, false, { 'p': prefix }, true ) .done(function(response) { if (response && response[0] && typeof response[0] !== 'undefined') { promise.resolve(response[0]); } else { promise.reject(response[0]); } }) .fail(function() { promise.reject(arguments[0]); }); return promise; } }; SharedLocalKVStorage.prototype.setItem = function(k, v, meta) { var self = this; if (self.broadcaster.crossTab.master) { var fn = "setItem"; if (typeof v === 'undefined') { fn = "removeItem"; } if (!meta) { // if triggered locally, by the master, there is no 'meta', so lets add our wdID meta = { 'origin': self.wdog.wdID }; } else { // if i'm not the one who triggered the change, trigger a local on change event. self.triggerOnChange(k, v); } // Notify via watchdog that there was a change! // doing it immediately (and not after .done), because of Chrome's delay of indexedDB operations self.wdog.notify("slkv_mchanged_" + self.name, {'k': k, 'v': v, 'meta': meta}); return self.persistAdapter[fn](k, v); } else { var promise = new MegaPromise(); if (typeof self._queuedSetOperations[k] === 'undefined') { self._queuedSetOperations[k] = []; } self._queuedSetOperations[k].push(promise); promise.targetValue = v; self.wdog.query( "slkv_set_" + self.name, SharedLocalKVStorage.DEFAULT_QUERY_TIMEOUT, false, { 'k': k, 'v': v }, true ) .done(function() { promise.resolve(v); }) .fail(function() { promise.reject(); }) .always(function() { var index = self._queuedSetOperations[k].indexOf(promise); if (index > -1) { self._queuedSetOperations[k].splice(index, 1); } }); return promise; } }; SharedLocalKVStorage.prototype.removeItem = function(k, meta) { var self = this; if (self.broadcaster.crossTab.master) { return self.setItem(k, undefined, meta); } else { var promise = new MegaPromise(); self.wdog.query( "slkv_set_" + self.name, SharedLocalKVStorage.DEFAULT_QUERY_TIMEOUT, false, { 'k': k, 'v': undefined }, true ) .done(function() { promise.resolve(); }) .fail(function() { promise.reject(); }); return promise; } }; SharedLocalKVStorage.prototype.clear = function() { var self = this; var promise = new MegaPromise(); var promises = []; (self.broadcaster.crossTab.master ? self.persistAdapter : self).keys().done(function(keys) { keys.forEach(function(k) { promises.push( self.removeItem(k) ); }); promise.linkDoneAndFailTo(MegaPromise.allDone(promises)); }) .fail(function() { promise.reject(); }); return promise; }; SharedLocalKVStorage.prototype.destroy = function(onlyIfMaster) { var self = this; if (self._leavingListener) { self.broadcaster.removeListener(self._leavingListener); } if (self.broadcaster.crossTab.master) { return this.persistAdapter.destroy(); } else if (!onlyIfMaster) { return self.clear(); } }; SharedLocalKVStorage.DB_MODE = { 'MANUAL_FLUSH': 1, 'NO_MEMOIZE': 2, 'FORCE_MEMOIZE': 4, 'BINARY': 8, }; SharedLocalKVStorage.DB_STATE = { 'NOT_READY': 0, 'READY': 1, 'INITIALISING': 2, 'FAILED': 3, }; SharedLocalKVStorage.encrypt = function(val) { 'use strict'; return FMDB.prototype.toStore(JSON.stringify(val)); }; SharedLocalKVStorage.decrypt = function(val) { 'use strict'; try { return JSON.parse(FMDB.prototype.fromStore(val)); } catch (e) { return ""; } }; SharedLocalKVStorage.Utils = Object.create(null); SharedLocalKVStorage.Utils.lazyInitCall = function(proto, method, master, fn) { 'use strict'; if (fn === undefined) { fn = master; master = true; } proto[method] = function __SLKVLazyInitCall() { var self = this; if (master && !mBroadcaster.crossTab.master) { // the method shall dealt with it. return fn.apply(self, arguments); } var args = toArray.apply(null, arguments); return new Promise(function(resolve, reject) { var name = self.__slkvLazyInitMutex || (self.__slkvLazyInitMutex = 'lIMutex' + makeUUID().slice(-13)); mutex.lock(name).then(function(unlock) { var onReadyState = function() { delete self.__slkvLazyInitMutex; return (self[method] = fn).apply(self, args).then(resolve).catch(reject); }; if (Object.hasOwnProperty.call(self, '__slkvLazyInitReady')) { return onReadyState().always(unlock); } self.lazyInit() .then(function() { Object.defineProperty(self, '__slkvLazyInitReady', {value: 1}); return onReadyState(); }) .always(unlock); }).catch(reject); }); }; return proto[method]; }; SharedLocalKVStorage.Utils._requiresMutex = function SLKVMutexWrapper(origFunc, methodName) { 'use strict'; return function __SLKVMutexWrapper() { var self = this; var args = toArray.apply(null, arguments); var name = this.__mutexLockName || (this.__mutexLockName = 'slkv' + makeUUID().slice(-13)); return new MegaPromise(function(resolve, reject) { mutex.lock(name) .then(function(unlock) { var wrap = function(dsp) { return function(arg) { if (d > 1) { self.logger.warn('Releasing lock(%s) from %s...', name, methodName); console.timeEnd(name); } unlock().always(dsp.bind(null, arg)).catch(reject); }; }; if (d > 1) { self.logger.warn('Lock(%s) acquired for %s...', name, methodName, [self, args]); console.time(name); } origFunc.apply(self, args).then(wrap(resolve)).catch(wrap(reject)); wrap = args = undefined; }) .catch(reject); }); }; }; SharedLocalKVStorage.Utils.DexieStorage = function(name, options) { 'use strict'; this.name = name; this.dbState = SharedLocalKVStorage.DB_STATE.NOT_READY; this.logger = new MegaLogger("SLKVDStorage[" + name + "]"); this.binary = options & SharedLocalKVStorage.DB_MODE.BINARY; this.manualFlush = options & SharedLocalKVStorage.DB_MODE.MANUAL_FLUSH; this.memoize = !(options & SharedLocalKVStorage.DB_MODE.NO_MEMOIZE); if (this.binary) { this.memoize = options & SharedLocalKVStorage.DB_MODE.FORCE_MEMOIZE; this._encryptValue = this._encryptBinaryValue; this._decryptValue = this._decryptBinaryValue; } this._reinitCache(); }; inherits(SharedLocalKVStorage.Utils.DexieStorage, MegaDataEmitter); /** * Database connection. * @name db * @memberOf SharedLocalKVStorage.Utils.DexieStorage.prototype */ lazy(SharedLocalKVStorage.Utils.DexieStorage.prototype, 'db', function() { 'use strict'; return new MegaDexie('SLKV', this.name, 'slkv_', true, {kv: '++i, &k'}); }); SharedLocalKVStorage.Utils._requiresDbReady = function SLKVDBConnRequired(fn) { 'use strict'; return function __requiresDBConnWrapper() { if (this.dbState === SharedLocalKVStorage.DB_STATE.READY) { return fn.apply(this, arguments); } var self = this; var args = toArray.apply(null, arguments); var promise = new MegaPromise(); if (!u_handle) { promise.reject(); return promise; } var success = function() { promise.linkDoneAndFailTo(fn.apply(self, args)); }; var failure = function(ex) { self.logger.warn(ex); self.dbState = SharedLocalKVStorage.DB_STATE.FAILED; promise.reject("DB_FAILED"); }; // lazy db init if (self.dbState === SharedLocalKVStorage.DB_STATE.NOT_READY) { self.dbState = SharedLocalKVStorage.DB_STATE.INITIALISING; self.dbLoadingPromise = new MegaPromise(); self.db.open().then(self._OpenDB.bind(self)).then(function(r) { self.logger.info('DB Ready, %d records loaded.', r.length, r); }).catch(failure).finally(function() { var p = self.dbLoadingPromise; delete self.dbLoadingPromise; if (d) { self.db.$__OwnerInstance = self; } if (self.dbState === SharedLocalKVStorage.DB_STATE.FAILED) { return p.reject("DB_OPEN_FAILED"); } self.dbState = SharedLocalKVStorage.DB_STATE.READY; success(); p.resolve(); }).catch(failure); } else if (self.dbState === SharedLocalKVStorage.DB_STATE.INITIALISING) { // DB open is in progress. self.dbLoadingPromise.then(success).catch(failure); } else { promise.reject("DB_FAILED"); } return promise; }; }; SharedLocalKVStorage.Utils.DexieStorage.prototype._encryptKey = SharedLocalKVStorage.encrypt; SharedLocalKVStorage.Utils.DexieStorage.prototype._decryptKey = SharedLocalKVStorage.decrypt; SharedLocalKVStorage.Utils.DexieStorage.prototype._encryptValue = SharedLocalKVStorage.encrypt; SharedLocalKVStorage.Utils.DexieStorage.prototype._decryptValue = SharedLocalKVStorage.decrypt; // @private SharedLocalKVStorage.Utils.DexieStorage.prototype._encryptBinaryValue = function(value) { 'use strict'; var pad = -value.byteLength & 15; if (pad) { var tmp = new Uint8Array(value.byteLength + pad); tmp.set(value); value = tmp; } return [pad, FMDB.prototype._crypt(u_k_aes, value)]; }; // @private SharedLocalKVStorage.Utils.DexieStorage.prototype._decryptBinaryValue = function(value) { 'use strict'; var pad = value[0]; value = FMDB.prototype._decrypt(u_k_aes, value[1]); return pad ? value.slice(0, -pad) : value; }; SharedLocalKVStorage.Utils.DexieStorage.prototype._OpenDB = function() { 'use strict'; var self = this; if (!this.memoize) { return Promise.resolve([]); } return self.db.kv.toArray() .then(function(r) { for (var i = 0; i < r.length; ++i) { self.dbcache[self._decryptKey(r[i].k)] = self._decryptValue(r[i].v); } return r; }); }; // flush new items / deletions to the DB (in channel 0, this should // be followed by call to setsn()) // will be a no-op if no fmdb set SharedLocalKVStorage.Utils.DexieStorage.prototype.flush = function() { 'use strict'; var self = this; var masterPromise = new MegaPromise(); var debug = function(o) { return o.map(function(o) { return self._decryptKey(o.k) + ':' + self._decryptValue(o.v); }); }; var done = onIdle.bind(null, function() { if (!self.memoize) { self._reinitCache(); } masterPromise.resolve(); }); var bulkDelete = Object.keys(self.delcache) .map(function(k) { delete self.dbcache[k]; return self.db.kv.where('k').equals(self._encryptKey(k)).delete(); }); var bulkPut = Object.keys(self.newcache) .map(function(k) { self.dbcache[k] = self.newcache[k]; return { k: self._encryptKey(k), v: self._encryptValue(self.newcache[k]) }; }); self.delcache = Object.create(null); self.newcache = Object.create(null); Promise.all(bulkDelete) .then(function() { return self.db.bulkUpdate(bulkPut); }) .then(done) .catch(function(ex) { if (d || is_karma) { self.db.kv.toArray() .then(function(o) { self.logger.error("flush failed", ex.message, [ex], debug(bulkPut), debug(o)); masterPromise.reject(ex); }); } else { masterPromise.reject(ex); } }); return masterPromise; }; SharedLocalKVStorage.Utils.DexieStorage.prototype.setItem = function __SLKVSetItem(k, v) { 'use strict'; console.assert(v !== undefined); delete this.delcache[k]; this.newcache[k] = v; if (this.manualFlush) { return MegaPromise.resolve(); } return this.flush(); }; // get item - if not found, promise will be rejected SharedLocalKVStorage.Utils.DexieStorage.prototype.getItem = function __SLKVGetItem(k) { 'use strict'; var self = this; return new MegaPromise(function(resolve, reject) { if (!self.delcache[k]) { if (self.newcache[k] !== undefined) { // record recently (over)written return resolve(self.newcache[k]); } // record available in DB if (self.dbcache[k] !== undefined) { return resolve(self.dbcache[k]); } } if (self.memoize) { // record deleted or unavailable return reject(); } self.db.kv.where('k').equals(self._encryptKey(k)).toArray() .then(function(r) { if (!r.length) { // record deleted or unavailable return reject(); } resolve(self._decryptValue(r[0].v)); }) .catch(reject); }); }; SharedLocalKVStorage.Utils.DexieStorage.prototype.keys = function __SLKVKeys(prefix) { 'use strict'; var self = this; return new MegaPromise(function(resolve, reject) { var filter = function(k) { return (prefix ? k.startsWith(prefix) : true) && self.delcache[k] === undefined; }; if (self.memoize) { var keys = Object.keys(Object.assign({}, self.dbcache, self.newcache)); return resolve(keys.filter(filter)); } self.db.kv.orderBy('k').keys() .then(function(keys) { resolve(keys.map(self._decryptKey.bind(self)).filter(filter)); }) .catch(reject); }); }; // check if item exists SharedLocalKVStorage.Utils.DexieStorage.prototype.hasItem = function __SLKVHasItem(k) { 'use strict'; var self = this; return new MegaPromise(function(resolve, reject) { if (!self.delcache[k] && (self.newcache[k] !== undefined || self.dbcache[k] !== undefined)) { return resolve(); } if (self.memoize) { return reject(); } self.db.kv.where('k').equals(self._encryptKey(k)).keys() .then(function(r) { if (r.length) { return resolve(); } reject(); }) .catch(reject); }); }; SharedLocalKVStorage.Utils.DexieStorage.prototype.removeItem = function __SLKVRemoveItem(k, expunge) { 'use strict'; var self = this; expunge = expunge === true; if (!expunge && self.memoize && this.newcache[k] === undefined && this.dbcache[k] === undefined) { return MegaPromise.reject(); } this.delcache[k] = true; delete this.newcache[k]; delete this.dbcache[k]; if (!expunge) { return this.flush(); } return new MegaPromise(function(resolve, reject) { self.flush().then(function() { return self.db.kv.count(); }).then(function(num) { if (d && !num) { console.assert(!$.len(Object.assign({}, self.dbcache, self.newcache))); } return num ? num : self._destroy(); }).then(resolve).catch(reject); }); }; /** * Iterate over all items, with prefix. * * Note: Case sensitive. * * @param prefix {String} prefix that would be used for filtering the data * @param cb {Function} cb(value, key) * @returns {MegaPromise} promise */ SharedLocalKVStorage.Utils.DexieStorage.prototype.eachPrefixItem = function __SLKVEachItem(prefix, cb) { 'use strict'; var self = this; return new MegaPromise(function(resolve, reject) { var feedback = function(store) { var keys = Object.keys(store); var keyl = keys.length; for (var i = 0; i < keyl; i++) { var k = keys[i]; if (!self.delcache[k] && k.startsWith(prefix)) { cb(store[k], k); } } }; if (self.memoize) { feedback(Object.assign({}, self.dbcache, self.newcache)); return resolve(); } self.db.kv.toArray() .then(function(r) { var store = Object.create(null); for (var i = r.length; i--;) { store[self._decryptKey(r[i].k)] = self._decryptValue(r[i].v); } feedback(store); resolve(); }) .catch(reject); }); }; /** * Drops the local db */ SharedLocalKVStorage.Utils.DexieStorage.prototype.destroy = function __SLKVDestroy() { var self = this; var promise = new MegaPromise(); self._reinitCache(); self.dbState = SharedLocalKVStorage.DB_STATE.NOT_READY; return promise.linkDoneAndFailTo(self.db.delete()); }; /** * Re/Initialises the local in memory cache */ SharedLocalKVStorage.Utils.DexieStorage.prototype._reinitCache = function __SLKVReinitCache() { 'use strict'; this.dbcache = Object.create(null); // items that reside in the DB this.newcache = Object.create(null); // new items that are pending flushing to the DB this.delcache = Object.create(null); // delete items that are pending deletion from the DB }; /** * Clear DB contents. * @returns {MegaPromise} */ SharedLocalKVStorage.Utils.DexieStorage.prototype.clear = function __SLKVClear() { var self = this; var promise = new MegaPromise(); self.db.kv.clear() .catch(function (e) { self.logger.error("clear failed: ", arguments, e.stack); self._reinitCache(); promise.reject(e); }) .finally(function () { self._reinitCache(); promise.resolve(); }); return promise; }; SharedLocalKVStorage.Utils.DexieStorage.prototype.close = function __SLKVClose() { var self = this; var oldState = self.dbState; self.dbState = SharedLocalKVStorage.DB_STATE.NOT_READY; if (oldState === SharedLocalKVStorage.DB_STATE.READY) { self.db.close(); } self.db = null; self._reinitCache(); }; /** * So that the code in the file is more easy to debug via IDEs, the * SharedLocalKVStorage.Utils.DexieStorage._requiresDbReady wrapper is going to wrap the required functions in runtime * Guarantee that promise-returning methods are executed one after another. */ (function __monkeyPatch(proto) { 'use strict'; // eslint-disable-next-line local-rules/misc-warnings Object.keys(proto) .filter(function(n) { return n[0] !== '_'; }) .forEach(function(methodName) { var origFunc = SharedLocalKVStorage.Utils._requiresDbReady(proto[methodName], methodName); if (methodName !== 'flush') { if (methodName === 'destroy' /* || ... */) { // to be used under an already acquired lock. Object.defineProperty(proto, '_' + methodName, {value: proto[methodName]}); } origFunc = SharedLocalKVStorage.Utils._requiresMutex(origFunc, methodName); } proto[methodName] = origFunc; var short = methodName.replace(/[A-Z].*/, ''); if (short !== methodName) { Object.defineProperty(proto, short, {value: proto[methodName]}); } }); })(SharedLocalKVStorage.Utils.DexieStorage.prototype); /** * @fileOverview * Storage of key/value pairs in a "container". */ var tlvstore = (function () { "use strict"; /** * @description *
Storage of key/value pairs in a "container".
* ** Stores a set of key/value pairs in a binary container format suitable for * encrypted storage of private attributes
* ** TLV records start with the key as a "tag" (ASCII string), terminated by a * NULL character (\u0000). The length of the payload is encoded as a 16-bit * unsigned integer in big endian format (2 bytes), followed by the payload * (as a byte string). The payload *must* contain 8-bit values for each * character only!
*/ var ns = {}; ns._logger = MegaLogger.getLogger('tlvstore'); /** * Generates a binary encoded TLV record from a key-value pair. * * @param key {string} * ASCII string label of record's key. * @param value {string} * Byte string payload of record. * @returns {string} * Single binary encoded TLV record. * @private */ ns.toTlvRecord = function(key, value) { var length = String.fromCharCode(value.length >>> 8) + String.fromCharCode(value.length & 0xff); return key + '\u0000' + length + value; }; /** * Generates a binary encoded TLV element from a key-value pair. * There is no separator in between and the length is fixted 2 bytes. * If the length of the value is bigger than 0xffff, then it will use 0xffff * as the length, and append the value after. * * @param key {string} * ASCII string label of record's key. * @param value {string} * Byte string payload of record. * @returns {string} * Single binary encoded TLV record. * @private */ ns.toTlvElement = function(key, value) { var length = String.fromCharCode(value.length >>> 8) + String.fromCharCode(value.length & 0xff); if (value.length > 0xffff) { length = String.fromCharCode(0xff) + String.fromCharCode(0xff); } return key + length + value; }; /** * Generates a binary encoded TLV record container from an object containing * key-value pairs. * * @param container {object} * Object containing (non-nested) key-value pairs. The keys have to be ASCII * strings, the values byte strings. * @returns {string} * Single binary encoded container of TLV records. */ ns.containerToTlvRecords = function(container) { var result = ''; for (var key in container) { if (container.hasOwnProperty(key)) { if (typeof container[key] === "number") { console.error("Found element in container with key: ", key, " which value is a number. Only " + "strings are allowed!"); return false; } result += ns.toTlvRecord(key, container[key]); } } return result; }; /** * Splits and decodes a TLV record off of a container into a key-value pair and * returns the record and the rest. * * @param tlvContainer {String} * Single binary encoded container of TLV records. * @returns {Object|Boolean} * Object containing two elements: `record` contains an array of two * elements (key and value of the decoded TLV record) and `rest` containing * the remainder of the tlvContainer still to decode. In case of decoding * errors, `false` is returned. */ ns.splitSingleTlvRecord = function(tlvContainer) { var keyLength = tlvContainer.indexOf('\u0000'); var key = tlvContainer.substring(0, keyLength); var valueLength = (tlvContainer.charCodeAt(keyLength + 1)) << 8 | tlvContainer.charCodeAt(keyLength + 2); var value = tlvContainer.substring(keyLength + 3, keyLength + valueLength + 3); var rest = tlvContainer.substring(keyLength + valueLength + 3); // Consistency checks. if ((valueLength !== value.length) || (rest.length !== tlvContainer.length - (keyLength + valueLength + 3))) { ns._logger.info('Inconsistent TLV decoding. Maybe content UTF-8 encoded?'); return false; } return { 'record': [key, value], 'rest': rest }; }; /** * Splits and decodes a TLV element off of a container into a key-value pair and * returns the element and the rest. * Note: if the length is 0xffff, which means the appended value is longer than 0xffff, * it means the rest is the value. * * @param tlvContainer {String} * Single binary encoded container of TLV elements. * @returns {Object|Boolean} * Object containing two parts: `element` contains an array of two * (key and value of the decoded TLV element) and `rest` containing * the remainder of the tlvContainer still to decode. In case of decoding * errors, `false` is returned. */ ns.splitSingleTlvElement = function(tlvContainer) { var keyLength = 1; var key = tlvContainer.substring(0, keyLength); var valueLength = (tlvContainer.charCodeAt(keyLength)) << 8 | tlvContainer.charCodeAt(keyLength + 1); var value = tlvContainer.substring(keyLength + 2, keyLength + valueLength + 2); if (valueLength === 0xffff) { value = tlvContainer.substring(keyLength + 2); valueLength = value.length; } var rest = tlvContainer.substring(keyLength + valueLength + 2); // Consistency checks. if ((valueLength !== value.length) || (rest.length !== tlvContainer.length - (keyLength + valueLength + 2))) { ns._logger.info('Inconsistent TLV decoding. Maybe content UTF-8 encoded?'); return false; } return { 'record': [key, value], 'rest': rest }; }; /** * Decodes a binary encoded container of TLV records into an object * representation. * * @param tlvContainer {String} * Single binary encoded container of TLV records. * @param [utf8LegacySafe] {Boolean} * Single binary encoded container of TLV records. * @returns {Object|Boolean} * Object containing (non-nested) key-value pairs. `false` in case of * failing TLV decoding. */ ns.tlvRecordsToContainer = function(tlvContainer, utf8LegacySafe) { var rest = tlvContainer; var container = {}; while (rest.length > 0) { var result = ns.splitSingleTlvRecord(rest); if (result === false) { container = false; break; } container[result.record[0]] = result.record[1]; rest = result.rest; } if (utf8LegacySafe && (container === false)) { // Treat the legacy case and first UTF-8 decode the container content. ns._logger.info('Retrying to decode TLV container legacy style ...'); return ns.tlvRecordsToContainer(from8(tlvContainer), false); } return container; }; /** * "Enumeration" of block cipher encryption schemes for private attribute * containers. * * @property AES_CCM_12_16 {integer} * AES in CCM mode, 12 byte IV/nonce and 16 byte MAC. * @property AES_CCM_10_16 {integer} * AES in CCM mode, 10 byte IV/nonce and 16 byte MAC. * @property AES_CCM_10_08 {integer} * AES in CCM mode, 10 byte IV/nonce and 8 byte MAC. * @property AES_GCM_12_16 {integer} * AES in CCM mode, 12 byte IV/nonce and 16 byte MAC. * @property AES_GCM_10_08 {integer} * AES in CCM mode, 10 byte IV/nonce and 8 byte MAC. */ ns.BLOCK_ENCRYPTION_SCHEME = { AES_CCM_12_16: 0x00, AES_CCM_10_16: 0x01, AES_CCM_10_08: 0x02, AES_GCM_12_16_BROKEN: 0x03, // Same as 0x00 (not GCM, due to a legacy bug). AES_GCM_10_08_BROKEN: 0x04, // Same as 0x02 (not GCM, due to a legacy bug). AES_GCM_12_16: 0x10, AES_GCM_10_08: 0x11 }; /** * Parameters for supported block cipher encryption schemes. */ ns.BLOCK_ENCRYPTION_PARAMETERS = { 0x00: {nonceSize: 12, macSize: 16, cipher: 'AES_CCM'}, // BLOCK_ENCRYPTION_SCHEME.AES_CCM_12_16 0x01: {nonceSize: 10, macSize: 16, cipher: 'AES_CCM'}, // BLOCK_ENCRYPTION_SCHEME.AES_CCM_10_16 0x02: {nonceSize: 10, macSize: 8, cipher: 'AES_CCM'}, // BLOCK_ENCRYPTION_SCHEME.AES_CCM_10_08 0x03: {nonceSize: 12, macSize: 16, cipher: 'AES_CCM'}, // Same as 0x00 (due to a legacy bug). 0x04: {nonceSize: 10, macSize: 8, cipher: 'AES_CCM'}, // Same as 0x02 (due to a legacy bug). 0x10: {nonceSize: 12, macSize: 16, cipher: 'AES_GCM'}, // BLOCK_ENCRYPTION_SCHEME.AES_GCM_12_16 0x11: {nonceSize: 10, macSize: 8, cipher: 'AES_GCM'} // BLOCK_ENCRYPTION_SCHEME.AES_GCM_10_08 }; /** * Encrypts clear text data to an authenticated ciphertext, armoured with * encryption mode indicator and IV. * * @param clearText {String} * Clear text as byte string. * @param key {String} * Encryption key as byte string. * @param mode {Number} * Encryption mode as an integer. One of tlvstore.BLOCK_ENCRYPTION_SCHEME. * @param [utf8Convert] {Boolean} * Perform UTF-8 conversion of clear text before encryption (default: false). * @returns {String} * Encrypted data block as byte string, incorporating mode, nonce and MAC. */ ns.blockEncrypt = function(clearText, key, mode, utf8Convert) { var nonceSize = ns.BLOCK_ENCRYPTION_PARAMETERS[mode].nonceSize; var tagSize = ns.BLOCK_ENCRYPTION_PARAMETERS[mode].macSize; var cipher = asmCrypto[ns.BLOCK_ENCRYPTION_PARAMETERS[mode].cipher]; var nonceBytes = new Uint8Array(nonceSize); asmCrypto.getRandomValues(nonceBytes); if (Array.isArray(key)) { // Key is in the form of an array of four 32-bit words. key = a32_to_str(key); } var keyBytes = asmCrypto.string_to_bytes(key); var clearBytes = asmCrypto.string_to_bytes( utf8Convert ? to8(clearText) : clearText); var cipherBytes = cipher.encrypt(clearBytes, keyBytes, nonceBytes, undefined, tagSize); return String.fromCharCode(mode) + asmCrypto.bytes_to_string(nonceBytes) + asmCrypto.bytes_to_string(cipherBytes); }; /** * Decrypts an authenticated cipher text armoured with a mode indicator and IV * to clear text data. * * @param cipherText {String} * Encrypted data block as byte string, incorporating mode, nonce and MAC. * @param key {String} * Encryption key as byte string. * @param [utf8Convert] {Boolean} * Perform UTF-8 conversion of clear text after decryption (default: false). * @returns {String} * Clear text as byte string. */ ns.blockDecrypt = function(cipherText, key, utf8Convert) { var mode = cipherText.charCodeAt(0); var nonceSize = ns.BLOCK_ENCRYPTION_PARAMETERS[mode].nonceSize; var nonceBytes = asmCrypto.string_to_bytes(cipherText.substring(1, nonceSize + 1)); var cipherBytes = asmCrypto.string_to_bytes(cipherText.substring(nonceSize + 1)); var tagSize = ns.BLOCK_ENCRYPTION_PARAMETERS[mode].macSize; var cipher = asmCrypto[ns.BLOCK_ENCRYPTION_PARAMETERS[mode].cipher]; if (Array.isArray(key)) { // Key is in the form of an array of four 32-bit words. key = a32_to_str(key); } var keyBytes = asmCrypto.string_to_bytes(key); var clearBytes = cipher.decrypt(cipherBytes, keyBytes, nonceBytes, undefined, tagSize); var clearText = asmCrypto.bytes_to_string(clearBytes); return utf8Convert ? from8(clearText) : clearText; }; return ns; }()); /** * The original jsbn.js code, but wrapped in a closure and added a code to ONLY export "BigInteger" into the * global (window) scope. */ (function(scope) { // Copyright (c) 2005 Tom Wu // All Rights Reserved. // See "LICENSE" for details. // Basic JavaScript BN library - subset useful for RSA encryption. // Bits per digit var dbits; // JavaScript engine analysis var canary = 0xdeadbeefcafe; var j_lm = ((canary&0xffffff)==0xefcafe); // (public) Constructor function BigInteger(a,b,c) { if(a != null) if("number" == typeof a) this.fromNumber(a,b,c); else if(b == null && "string" != typeof a) this.fromString(a,256); else this.fromString(a,b); } // return new, unset BigInteger function nbi() { return new BigInteger(null); } // am: Compute w_j += (x*this_i), propagate carries, // c is initial carry, returns final carry. // c < 3*dvalue, x < 2*dvalue, this_i < dvalue // We need to select the fastest one that works in this environment. // am1: use a single mult and divide to get the high bits, // max digit bits should be 26 because // max internal value = 2*dvalue^2-2*dvalue (< 2^53) function am1(i,x,w,j,c,n) { while(--n >= 0) { var v = x*this[i++]+w[j]+c; c = Math.floor(v/0x4000000); w[j++] = v&0x3ffffff; } return c; } // am2 avoids a big mult-and-extract completely. // Max digit bits should be <= 30 because we do bitwise ops // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) function am2(i,x,w,j,c,n) { var xl = x&0x7fff, xh = x>>15; while(--n >= 0) { var l = this[i]&0x7fff; var h = this[i++]>>15; var m = xh*l+h*xl; l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); w[j++] = l&0x3fffffff; } return c; } // Alternately, set max digit bits to 28 since some // browsers slow down when dealing with 32-bit numbers. function am3(i,x,w,j,c,n) { var xl = x&0x3fff, xh = x>>14; while(--n >= 0) { var l = this[i]&0x3fff; var h = this[i++]>>14; var m = xh*l+h*xl; l = xl*l+((m&0x3fff)<<14)+w[j]+c; c = (l>>28)+(m>>14)+xh*h; w[j++] = l&0xfffffff; } return c; } if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) { BigInteger.prototype.am = am2; dbits = 30; } else if(j_lm && (navigator.appName != "Netscape")) { BigInteger.prototype.am = am1; dbits = 26; } else { // Mozilla/Netscape seems to prefer am3 BigInteger.prototype.am = am3; dbits = 28; } BigInteger.prototype.DB = dbits; BigInteger.prototype.DM = ((1<>(p+=this.DB-k);
}
else {
d = (this[i]>>(p-=k))&km;
if(p <= 0) { p += this.DB; --i; }
}
if(d > 0) m = true;
if(m) r += int2char(d);
}
}
return m?r:"0";
}
// (public) -this
function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; }
// (public) |this|
function bnAbs() { return (this.s<0)?this.negate():this; }
// (public) return + if this > a, - if this < a, 0 if equal
function bnCompareTo(a) {
var r = this.s-a.s;
if(r != 0) return r;
var i = this.t;
r = i-a.t;
if(r != 0) return (this.s<0)?-r:r;
while(--i >= 0) if((r=this[i]-a[i]) != 0) return r;
return 0;
}
// returns bit length of the integer x
function nbits(x) {
var r = 1, t;
if((t=x>>>16) != 0) { x = t; r += 16; }
if((t=x>>8) != 0) { x = t; r += 8; }
if((t=x>>4) != 0) { x = t; r += 4; }
if((t=x>>2) != 0) { x = t; r += 2; }
if((t=x>>1) != 0) { x = t; r += 1; }
return r;
}
// (public) return the number of bits in "this"
function bnBitLength() {
if(this.t <= 0) return 0;
return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM));
}
// (protected) r = this << n*DB
function bnpDLShiftTo(n,r) {
var i;
for(i = this.t-1; i >= 0; --i) r[i+n] = this[i];
for(i = n-1; i >= 0; --i) r[i] = 0;
r.t = this.t+n;
r.s = this.s;
}
// (protected) r = this >> n*DB
function bnpDRShiftTo(n,r) {
for(var i = n; i < this.t; ++i) r[i-n] = this[i];
r.t = Math.max(this.t-n,0);
r.s = this.s;
}
// (protected) r = this << n
function bnpLShiftTo(n,r) {
var bs = n%this.DB;
var cbs = this.DB-bs;
var bm = (1< >(p+=this.DB-8);
}
else {
d = (this[i]>>(p-=8))&0xff;
if(p <= 0) { p += this.DB; --i; }
}
if((d&0x80) != 0) d |= -256;
if(k == 0 && (this.s&0x80) != (d&0x80)) ++k;
if(k > 0 || d != this.s) r[k++] = d;
}
}
return r;
}
function bnEquals(a) { return(this.compareTo(a)==0); }
function bnMin(a) { return(this.compareTo(a)<0)?this:a; }
function bnMax(a) { return(this.compareTo(a)>0)?this:a; }
// (protected) r = this op a (bitwise)
function bnpBitwiseTo(a,op,r) {
var i, f, m = Math.min(a.t,this.t);
for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]);
if(a.t < this.t) {
f = a.s&this.DM;
for(i = m; i < this.t; ++i) r[i] = op(this[i],f);
r.t = this.t;
}
else {
f = this.s&this.DM;
for(i = m; i < a.t; ++i) r[i] = op(f,a[i]);
r.t = a.t;
}
r.s = op(this.s,a.s);
r.clamp();
}
// (public) this & a
function op_and(x,y) { return x&y; }
function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; }
// (public) this | a
function op_or(x,y) { return x|y; }
function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; }
// (public) this ^ a
function op_xor(x,y) { return x^y; }
function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; }
// (public) this & ~a
function op_andnot(x,y) { return x&~y; }
function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; }
// (public) ~this
function bnNot() {
var r = nbi();
for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i];
r.t = this.t;
r.s = ~this.s;
return r;
}
// (public) this << n
function bnShiftLeft(n) {
var r = nbi();
if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r);
return r;
}
// (public) this >> n
function bnShiftRight(n) {
var r = nbi();
if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r);
return r;
}
// return index of lowest 1-bit in x, x < 2^31
function lbit(x) {
if(x == 0) return -1;
var r = 0;
if((x&0xffff) == 0) { x >>= 16; r += 16; }
if((x&0xff) == 0) { x >>= 8; r += 8; }
if((x&0xf) == 0) { x >>= 4; r += 4; }
if((x&3) == 0) { x >>= 2; r += 2; }
if((x&1) == 0) ++r;
return r;
}
// (public) returns index of lowest 1-bit (or -1 if none)
function bnGetLowestSetBit() {
for(var i = 0; i < this.t; ++i)
if(this[i] != 0) return i*this.DB+lbit(this[i]);
if(this.s < 0) return this.t*this.DB;
return -1;
}
// return number of 1 bits in x
function cbit(x) {
var r = 0;
while(x != 0) { x &= x-1; ++r; }
return r;
}
// (public) return number of set bits
function bnBitCount() {
var r = 0, x = this.s&this.DM;
for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x);
return r;
}
// (public) true iff nth bit is set
function bnTestBit(n) {
var j = Math.floor(n/this.DB);
if(j >= this.t) return(this.s!=0);
return((this[j]&(1<<(n%this.DB)))!=0);
}
// (protected) this op (1< Storage of authenticated contacts.
* A container (key ring) that keeps information of the authentication state
* for all authenticated contacts. Each record is indicated by the contact's
* userhandle as an attribute. The associated value is an object containing
* the authenticated `fingerprint` of the public key, the authentication
* `method` (e. g. `authring.AUTHENTICATION_METHOD.FINGERPRINT_COMPARISON`)
* and the key `confidence` (e. g. `authring.KEY_CONFIDENCE.UNSURE`).
* The records are stored in a concatenated fashion, with each user handle
* represented in its compact 8 byte form followed by a the fingerprint as a
* byte string and a "trust indicator" byte containing the authentication and
* confidence information. Therefore each authenticated user "consumes"
* 29 bytes of storage.
* Load contacts' authentication info with `authring.getContacts()` and save
* with `authring.setContacts()`.
')
.replace(/\[I\]/g, '').replace(/\[\/I\]/g, '')
.replace(/\[B\]/g, '').replace(/\[\/B\]/g, '')
.replace(/\[U]/g, '').replace(/\[\/U]/g, '')
.replace(/\[G]/g, '')
.replace(/\[\/G]/g, '');
};
var unmount = function() {
if ($currentNode) {
$currentNode.remove();
$currentNode = null;
$currentTriggerer.unbind(SIMPLETIP_UPDATED_EVENT);
$currentTriggerer.unbind(SIMPLETIP_CLOSE_EVENT);
$currentTriggerer = null;
}
};
$(document.body).rebind('mouseenter.simpletip', '.simpletip', function () {
var $this = $(this);
if ($currentNode) {
unmount();
}
if ($this.is('.deactivated') || $this.parent().is('.deactivated')) {
return false;
}
var contents = $this.attr('data-simpletip');
if (contents) {
var $node = $template.clone();
var $textContainer = $('span', $node);
$textContainer.safeHTML(sanitize(contents));
// Handle the tooltip's text content updates based on the current control state,
// e.g. "Mute" -> "Unmute"
$this.rebind(SIMPLETIP_UPDATED_EVENT, function() {
$textContainer.safeHTML(
sanitize($this.attr('data-simpletip'))
);
});
$this.rebind(SIMPLETIP_CLOSE_EVENT, function() {
unmount();
});
$('body').append($node);
$currentNode = $node;
$currentTriggerer = $this;
var wrapper = $this.attr('data-simpletipwrapper') || "";
if (wrapper) {
wrapper += ",";
}
var customStyle = $this.attr('data-simpletip-style');
if (customStyle) {
$currentNode.css(customStyle);
}
var customClass = $this.attr('data-simpletip-class');
if (customClass) {
$currentNode.addClass(customClass);
}
var my = "center top";
var at = "center bottom";
if ($this.attr('data-simpletipposition') === "top") {
my = "center bottom";
at = "center top";
}
$node.position({
of: $this,
my: my,
at: at,
collision: "flipfit",
within: $this.parents('.ps-container,' + wrapper + 'body').first(),
using: function(obj, info) {
var vertClass = info.vertical === "top" ? "b" : "t";
var horizClass = info.horizontal === "left" ? "r" : "l";
this.classList.remove(
"simpletip-v-t", "simpletip-v-b", "simpletip-h-l", "simpletip-h-r"
);
this.classList.add("simpletip-h-" + horizClass, "simpletip-v-" + vertClass, "visible");
var topOffset = 0;
if (vertClass === "t") {
topOffset = -6;
}
if ($this.attr('data-simpletipoffset')) {
var offset = parseInt($this.attr('data-simpletipoffset'), 10);
if (vertClass === "t") {
topOffset += offset * -1;
}
else {
topOffset += offset;
}
}
$(this).css({
left: obj.left + 'px',
top: obj.top + topOffset + 'px'
});
}
});
// Calculate Arrow position
var $tooltipArrow = $node.find('.tooltip-arrow');
$tooltipArrow.position({
of: $this,
my: my,
at: at,
collision: "none",
using: function(obj) {
$(this).css({
left: obj.left + 'px'
});
}
});
}
});
$(document.body).rebind('mouseover.simpletip, touchmove.simpletip', function(e) {
if ($currentNode && !e.target.classList.contains('simpletip') && !$(e.target).parents('.simpletip').length > 0
&& !e.target.classList.contains('tooltip-arrow')) {
unmount();
}
});
})(jQuery);
/**
* Handle all logic for rendering for users' avatar
*/
var useravatar = (function() {
'use strict';
var _colors = [
"#69F0AE",
"#13E03C",
"#31B500",
"#00897B",
"#00ACC1",
"#61D2FF",
"#2BA6DE",
"#FFD300",
"#FFA500",
"#FF6F00",
"#E65100",
"#FF5252",
"#FF1A53",
"#C51162",
"#880E4F"
];
var logger = MegaLogger.getLogger('useravatar');
/**
* List of TWO-letters avatars that we ever generated. It's useful to replace
* the moment we discover the real avatar associate with that avatar
*/
var _watching = {};
/**
* Public methods
*/
var ns = {};
/**
* Return a SVG image representing the Letter avatar
* @param {Object} user The user object or email
* @returns {String}
* @private
*/
function _getAvatarSVGDataURI(user) {
var s = _getAvatarProperties(user);
var $template = $('#avatar-svg').clone().removeClass('hidden')
.find('svg').addClass('color' + s.colorIndex).end()
.find('text').text(s.letters).end();
$template = window.btoa(to8($template.html()));
return 'data:image/svg+xml;base64,' + $template;
}
/**
* Return two letters and the color for a given string.
* @param {Object|String} user The user object or email
* @returns {Object}
* @private
*/
function _getAvatarProperties(user) {
user = String(user.u || user);
var name = M.getNameByHandle(user) || user;
if (name === user && M.suba[user] && M.suba[user].firstname) {
// Acquire the avatar matches the first letter for pending accounts in business account
name = from8(base64urldecode(M.suba[user].firstname)).trim();
}
var color = UH64(user).mod(_colors.length);
if (color === false) {
color = user.charCodeAt(0) % _colors.length;
}
return {letters: name.toUpperCase()[0], color: _colors[color], colorIndex: color + 1};
}
/**
* Return the HTML to represent a two letter avatar.
*
* @param {Object} user The user object or email
* @param {String} className Any extra CSS classes that we want to append to the HTML
* @param {String} element The HTML tag
* @returns {String} Returns the HTML
* @returns {Boolean} Adds addition blured background block
* @private
*/
function _getAvatarContent(user, className, element, bg) {
var id = user.u || user;
var bgBlock = '';
if (element === 'ximg') {
return _getAvatarSVGDataURI(user);
}
var s = _getAvatarProperties(user);
if (!_watching[id]) {
_watching[id] = {};
}
if (bg) {
bgBlock = '