eu.static.mega.co.nz Open in urlscan Pro
2a0b:e40:3::11  Public Scan

URL: https://eu.static.mega.co.nz/4/js/mega-4_fcbac362059d447a3e1d0c8bd4576711e45b778dfca4b41169c8cd71e58ebede.js
Submission: On June 18 via manual from CA — Scanned from NZ

Form analysis 0 forms found in the DOM

Text Content

/* Bundle Includes:
 *   js/vendor/dexie.js
 *   js/functions.js
 *   js/config.js
 *   js/crypto.js
 *   js/account.js
 *   js/security.js
 */

/*
 * Dexie.js - a minimalistic wrapper for IndexedDB
 * ===============================================
 *
 * By David Fahlander, david.fahlander@gmail.com
 *
 * Version 3.2.0.meganz, 2022-01-18T12:19:53.273Z
 *
 * https://dexie.org
 *
 * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/
 */

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Dexie = factory());
})(this, (function () { 'use strict';

const _global = typeof self !== 'undefined' ? self :
    typeof window !== 'undefined' ? window :
        global;

const keys = Object.keys;
const isArray = Array.isArray;
if (typeof Promise !== 'undefined' && !_global.Promise) {
    _global.Promise = Promise;
}
function extend(obj, extension) {
    if (typeof extension !== 'object')
        return obj;
    return Object.assign(obj, extension);
}
const getProto = Object.getPrototypeOf;
const _hasOwn = {}.hasOwnProperty;
function hasOwn(obj, prop) {
    return _hasOwn.call(obj, prop);
}
function props(proto, extension) {
    if (typeof extension === 'function')
        extension = extension(getProto(proto));
    const keys = Reflect.ownKeys(extension);
    for (let i = keys.length; i--;) {
        setProp(proto, keys[i], extension[keys[i]]);
    }
}
const defineProperty = Object.defineProperty;
function setProp(obj, prop, functionOrGetSet, options) {
    defineProperty(obj, prop, extend(functionOrGetSet && hasOwn(functionOrGetSet, "get") && typeof functionOrGetSet.get === 'function' ?
        { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } :
        { value: functionOrGetSet, configurable: true, writable: true }, options));
}
function derive(Child) {
    return {
        from: function (Parent) {
            Child.prototype = Object.create(Parent.prototype);
            setProp(Child.prototype, "constructor", Child);
            return {
                extend: props.bind(null, Child.prototype)
            };
        }
    };
}
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
function getPropertyDescriptor(obj, prop) {
    const pd = getOwnPropertyDescriptor(obj, prop);
    let proto;
    return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop);
}
const _slice = [].slice;
function slice(args, start, end) {
    return _slice.call(args, start, end);
}
function override(origFunc, overridedFactory) {
    return overridedFactory(origFunc);
}
function assert(b) {
    if (!b)
        throw new Error("Assertion Failed");
}
function asap$1(fn) {
    queueMicrotask(fn);
}
function arrayToObject(array, extractor) {
    return array.reduce((result, item, i) => {
        var nameAndValue = extractor(item, i);
        if (nameAndValue)
            result[nameAndValue[0]] = nameAndValue[1];
        return result;
    }, {});
}
function tryCatch(fn, onerror, args) {
    try {
        fn.apply(null, args);
    }
    catch (ex) {
        onerror && onerror(ex);
    }
}
function getByKeyPath(obj, keyPath) {
    if (hasOwn(obj, keyPath))
        return obj[keyPath];
    if (!keyPath)
        return obj;
    if (typeof keyPath !== 'string') {
        var rv = [];
        for (var i = 0, l = keyPath.length; i < l; ++i) {
            var val = getByKeyPath(obj, keyPath[i]);
            rv.push(val);
        }
        return rv;
    }
    var period = keyPath.indexOf('.');
    if (period !== -1) {
        var innerObj = obj[keyPath.substr(0, period)];
        return innerObj === undefined ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1));
    }
    return undefined;
}
function setByKeyPath(obj, keyPath, value) {
    if (!obj || keyPath === undefined)
        return;
    if ('isFrozen' in Object && Object.isFrozen(obj))
        return;
    if (typeof keyPath !== 'string' && 'length' in keyPath) {
        assert(typeof value !== 'string' && 'length' in value);
        for (var i = 0, l = keyPath.length; i < l; ++i) {
            setByKeyPath(obj, keyPath[i], value[i]);
        }
    }
    else {
        var period = keyPath.indexOf('.');
        if (period !== -1) {
            var currentKeyPath = keyPath.substr(0, period);
            var remainingKeyPath = keyPath.substr(period + 1);
            if (remainingKeyPath === "")
                if (value === undefined) {
                    if (isArray(obj) && !isNaN(parseInt(currentKeyPath)))
                        obj.splice(currentKeyPath, 1);
                    else
                        delete obj[currentKeyPath];
                }
                else
                    obj[currentKeyPath] = value;
            else {
                var innerObj = obj[currentKeyPath];
                if (!innerObj)
                    innerObj = (obj[currentKeyPath] = {});
                setByKeyPath(innerObj, remainingKeyPath, value);
            }
        }
        else {
            if (value === undefined) {
                if (isArray(obj) && !isNaN(parseInt(keyPath)))
                    obj.splice(keyPath, 1);
                else
                    delete obj[keyPath];
            }
            else
                obj[keyPath] = value;
        }
    }
}
function delByKeyPath(obj, keyPath) {
    if (typeof keyPath === 'string')
        setByKeyPath(obj, keyPath, undefined);
    else if ('length' in keyPath)
        [].map.call(keyPath, function (kp) {
            setByKeyPath(obj, kp, undefined);
        });
}
function shallowClone(obj) {
    return ({ ...obj });
}
const concat = [].concat;
function flatten(a) {
    return concat.apply([], a);
}
const intrinsicTypeNames = "Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey"
    .split(',').concat(flatten([8, 16, 32, 64].map(num => ["Int", "Uint", "Float"].map(t => t + num + "Array")))).filter(t => _global[t]);
const intrinsicTypes = intrinsicTypeNames.map(t => _global[t]);
arrayToObject(intrinsicTypeNames, x => [x, true]);
let circularRefs = null;
function deepClone(any) {
    circularRefs = new WeakMap();
    const rv = innerDeepClone(any);
    circularRefs = null;
    return rv;
}
function innerDeepClone(any) {
    if (!any || typeof any !== 'object')
        return any;
    let rv = circularRefs && circularRefs.get(any);
    if (rv)
        return rv;
    if (isArray(any)) {
        rv = [];
        circularRefs && circularRefs.set(any, rv);
        for (let i = 0, l = any.length; i < l; ++i) {
            rv.push(innerDeepClone(any[i]));
        }
    }
    else if (intrinsicTypes.indexOf(any.constructor) >= 0) {
        rv = any;
    }
    else {
        const proto = getProto(any);
        rv = proto === Object.prototype ? {} : Object.create(proto);
        circularRefs && circularRefs.set(any, rv);
        for (let prop in any) {
            if (hasOwn(any, prop)) {
                rv[prop] = innerDeepClone(any[prop]);
            }
        }
    }
    return rv;
}
const { toString } = {};
function toStringTag(o) {
    return toString.call(o).slice(8, -1);
}
function getIteratorOf(x) {
    let i;
    return x != null && (i = x[Symbol.iterator]) && i.apply(x);
}
const NO_CHAR_ARRAY = {};
function getArrayOf(arrayLike) {
    var i, a, x, it;
    if (arguments.length === 1) {
        if (isArray(arrayLike))
            return arrayLike.slice();
        if (this === NO_CHAR_ARRAY && typeof arrayLike === 'string')
            return [arrayLike];
        if ((it = getIteratorOf(arrayLike))) {
            a = [];
            while ((x = it.next()), !x.done)
                a.push(x.value);
            return a;
        }
        if (arrayLike == null)
            return [arrayLike];
        i = arrayLike.length;
        if (typeof i === 'number') {
            a = new Array(i);
            while (i--)
                a[i] = arrayLike[i];
            return a;
        }
        return [arrayLike];
    }
    i = arguments.length;
    a = new Array(i);
    while (i--)
        a[i] = arguments[i];
    return a;
}
const isAsyncFunction = typeof Symbol !== 'undefined'
    ? (fn) => fn[Symbol.toStringTag] === 'AsyncFunction'
    : () => false;

var debug = typeof localStorage === 'object' && !!localStorage.dexieDebug;
function setDebug(value, filter) {
    debug = value;
    libraryFilter = filter;
}
var libraryFilter = () => true;
function getErrorWithStack() {
    return new Error();
}
function prettyStack(exception, numIgnoredFrames) {
    var stack = exception.stack;
    if (!stack)
        return "";
    numIgnoredFrames = (numIgnoredFrames || 0);
    if (stack.indexOf(exception.name) === 0)
        numIgnoredFrames += (exception.name + exception.message).split('\n').length;
    return stack.split('\n')
        .slice(numIgnoredFrames)
        .filter(libraryFilter)
        .map(frame => "\n" + frame)
        .join('');
}

var dexieErrorNames = [
    'Modify',
    'Bulk',
    'OpenFailed',
    'VersionChange',
    'Schema',
    'Upgrade',
    'InvalidTable',
    'MissingAPI',
    'NoSuchDatabase',
    'InvalidArgument',
    'SubTransaction',
    'Unsupported',
    'Internal',
    'DatabaseClosed',
    'PrematureCommit',
    'ForeignAwait'
];
var idbDomErrorNames = [
    'Unknown',
    'Constraint',
    'Data',
    'TransactionInactive',
    'ReadOnly',
    'Version',
    'NotFound',
    'InvalidState',
    'InvalidAccess',
    'Abort',
    'Timeout',
    'QuotaExceeded',
    'Syntax',
    'DataClone'
];
var errorList = dexieErrorNames.concat(idbDomErrorNames);
var defaultTexts = {
    VersionChanged: "Database version changed by other database connection",
    DatabaseClosed: "Database has been closed",
    Abort: "Transaction aborted",
    TransactionInactive: "Transaction has already completed or failed",
    MissingAPI: "IndexedDB API missing."
};
function DexieError(name, msg) {
    this._e = getErrorWithStack();
    this.name = name;
    this.message = msg;
}
derive(DexieError).from(Error).extend({
    stack: {
        get: function () {
            return this._stack ||
                (this._stack = this.name + ": " + this.message + prettyStack(this._e, 2));
        }
    },
    toString: function () { return this.name + ": " + this.message; }
});
function getMultiErrorMessage(msg, failures) {
    return msg + ". Errors: " + Object.keys(failures)
        .map(key => failures[key].toString())
        .filter((v, i, s) => s.indexOf(v) === i)
        .join('\n');
}
function ModifyError(msg, failures, successCount, failedKeys) {
    this._e = getErrorWithStack();
    this.failures = failures;
    this.failedKeys = failedKeys;
    this.successCount = successCount;
    this.message = getMultiErrorMessage(msg, failures);
}
derive(ModifyError).from(DexieError);
function BulkError(msg, failures) {
    this._e = getErrorWithStack();
    this.name = "BulkError";
    this.failures = Object.keys(failures).map(pos => failures[pos]);
    this.failuresByPos = failures;
    this.message = getMultiErrorMessage(msg, failures);
}
derive(BulkError).from(DexieError);
var errnames = errorList.reduce((obj, name) => (obj[name] = name + "Error", obj), {});
const BaseException = DexieError;
var exceptions = errorList.reduce((obj, name) => {
    var fullName = name + "Error";
    function DexieError(msgOrInner, inner) {
        this._e = getErrorWithStack();
        this.name = fullName;
        if (!msgOrInner) {
            this.message = defaultTexts[name] || fullName;
            this.inner = null;
        }
        else if (typeof msgOrInner === 'string') {
            this.message = `${msgOrInner}${!inner ? '' : '\n ' + inner}`;
            this.inner = inner || null;
        }
        else if (typeof msgOrInner === 'object') {
            this.message = `${msgOrInner.name} ${msgOrInner.message}`;
            this.inner = msgOrInner;
        }
    }
    derive(DexieError).from(BaseException);
    obj[name] = DexieError;
    return obj;
}, {});
exceptions.Syntax = SyntaxError;
exceptions.Type = TypeError;
exceptions.Range = RangeError;
var exceptionMap = idbDomErrorNames.reduce((obj, name) => {
    obj[name + "Error"] = exceptions[name];
    return obj;
}, {});
function mapError(domError, message) {
    if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name])
        return domError;
    var rv = new exceptionMap[domError.name](message || domError.message, domError);
    if ("stack" in domError) {
        setProp(rv, "stack", { get: function () {
                return this.inner.stack;
            } });
    }
    return rv;
}
var fullNameExceptions = errorList.reduce((obj, name) => {
    if (["Syntax", "Type", "Range"].indexOf(name) === -1)
        obj[name + "Error"] = exceptions[name];
    return obj;
}, {});
fullNameExceptions.ModifyError = ModifyError;
fullNameExceptions.DexieError = DexieError;
fullNameExceptions.BulkError = BulkError;

function nop() { }
function mirror(val) { return val; }
function pureFunctionChain(f1, f2) {
    if (f1 == null || f1 === mirror)
        return f2;
    return function (val) {
        return f2(f1(val));
    };
}
function callBoth(on1, on2) {
    return function () {
        on1.apply(this, arguments);
        on2.apply(this, arguments);
    };
}
function hookCreatingChain(f1, f2) {
    if (f1 === nop)
        return f2;
    return function () {
        var res = f1.apply(this, arguments);
        if (res !== undefined)
            arguments[0] = res;
        var onsuccess = this.onsuccess,
        onerror = this.onerror;
        this.onsuccess = null;
        this.onerror = null;
        var res2 = f2.apply(this, arguments);
        if (onsuccess)
            this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
        if (onerror)
            this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
        return res2 !== undefined ? res2 : res;
    };
}
function hookDeletingChain(f1, f2) {
    if (f1 === nop)
        return f2;
    return function () {
        f1.apply(this, arguments);
        var onsuccess = this.onsuccess,
        onerror = this.onerror;
        this.onsuccess = this.onerror = null;
        f2.apply(this, arguments);
        if (onsuccess)
            this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
        if (onerror)
            this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
    };
}
function hookUpdatingChain(f1, f2) {
    if (f1 === nop)
        return f2;
    return function (modifications) {
        var res = f1.apply(this, arguments);
        extend(modifications, res);
        var onsuccess = this.onsuccess,
        onerror = this.onerror;
        this.onsuccess = null;
        this.onerror = null;
        var res2 = f2.apply(this, arguments);
        if (onsuccess)
            this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess;
        if (onerror)
            this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror;
        return res === undefined ?
            (res2 === undefined ? undefined : res2) :
            (extend(res, res2));
    };
}
function reverseStoppableEventChain(f1, f2) {
    if (f1 === nop)
        return f2;
    return function () {
        if (f2.apply(this, arguments) === false)
            return false;
        return f1.apply(this, arguments);
    };
}
function promisableChain(f1, f2) {
    if (f1 === nop)
        return f2;
    return function () {
        var res = f1.apply(this, arguments);
        if (res && typeof res.then === 'function') {
            var thiz = this, i = arguments.length, args = new Array(i);
            while (i--)
                args[i] = arguments[i];
            return res.then(function () {
                return f2.apply(thiz, args);
            });
        }
        return f2.apply(this, arguments);
    };
}

var INTERNAL = {};
const LONG_STACKS_CLIP_LIMIT = 100,
MAX_LONG_STACKS = 20, ZONE_ECHO_LIMIT = 100, [resolvedNativePromise, nativePromiseProto, resolvedGlobalPromise] = (() => {
    let globalP = Promise.resolve();
    if (typeof crypto === 'undefined' || !crypto.subtle)
        return [globalP, getProto(globalP), globalP];
    const nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0]));
    return [
        nativeP,
        getProto(nativeP),
        globalP
    ];
})();
const NativePromise = resolvedNativePromise && resolvedNativePromise.constructor;
const patchGlobalPromise = !!resolvedGlobalPromise;
var stack_being_generated = false;
const schedulePhysicalTick = () => {
    queueMicrotask(physicalTick);
};
var asap = function (callback, args) {
    microtickQueue.push([callback, args]);
    if (needsNewPhysicalTick) {
        schedulePhysicalTick();
        needsNewPhysicalTick = false;
    }
};
var isOutsideMicroTick = true,
needsNewPhysicalTick = true,
unhandledErrors = [],
rejectingErrors = [],
currentFulfiller = null, rejectionMapper = mirror;
var globalPSD = {
    id: 'global',
    global: true,
    ref: 0,
    unhandleds: [],
    onunhandled: globalError,
    pgp: false,
    env: {},
    finalize: function () {
        this.unhandleds.forEach(uh => {
            try {
                globalError(uh[0], uh[1]);
            }
            catch (e) { }
        });
    }
};
var PSD = globalPSD;
var microtickQueue = [];
var numScheduledCalls = 0;
var tickFinalizers = [];
function DexiePromise(fn) {
    this._listeners = [];
    this.onuncatched = nop;
    this._lib = false;
    var psd = (this._PSD = PSD);
    if (debug) {
        this._stackHolder = getErrorWithStack();
        this._prev = null;
        this._numPrev = 0;
    }
    if (typeof fn !== 'function') {
        if (fn !== INTERNAL)
            throw new TypeError('Not a function');
        this._state = arguments[1];
        this._value = arguments[2];
        if (this._state === false)
            handleRejection(this, this._value);
        return;
    }
    this._state = null;
    this._value = null;
    ++psd.ref;
    executePromiseTask(this, fn);
}
const thenProp = {
    get: function () {
        var psd = PSD, microTaskId = totalEchoes;
        function then(onFulfilled, onRejected) {
            var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes);
            const cleanup = possibleAwait && !decrementExpectedAwaits();
            var rv = new DexiePromise((resolve, reject) => {
                propagateToListener(this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd));
            });
            debug && linkToPreviousPromise(rv, this);
            return rv;
        }
        then.prototype = INTERNAL;
        return then;
    },
    set: function (value) {
        setProp(this, 'then', value && value.prototype === INTERNAL ?
            thenProp :
            {
                get: function () {
                    return value;
                },
                set: thenProp.set
            });
    }
};
props(DexiePromise.prototype, {
    then: thenProp,
    _then: function (onFulfilled, onRejected) {
        propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD));
    },
    dump: NativePromise.prototype.dump,
    always: NativePromise.prototype.always,
    catch: function (onRejected) {
        if (arguments.length === 1)
            return this.then(null, onRejected);
        var type = arguments[0], handler = arguments[1];
        return typeof type === 'function' ? this.then(null, err =>
        err instanceof type ? handler(err) : PromiseReject(err))
            : this.then(null, err =>
            err && err.name === type ? handler(err) : PromiseReject(err));
    },
    finally: function (onFinally) {
        return this.then(value => {
            onFinally();
            return value;
        }, err => {
            onFinally();
            return PromiseReject(err);
        });
    },
    stack: {
        get: function () {
            if (this._stack)
                return this._stack;
            try {
                stack_being_generated = true;
                var stacks = getStack(this, [], MAX_LONG_STACKS);
                var stack = stacks.join("\nFrom previous: ");
                if (this._state !== null)
                    this._stack = stack;
                return stack;
            }
            finally {
                stack_being_generated = false;
            }
        }
    },
    timeout: function (ms, msg) {
        return ms < Infinity ?
            new DexiePromise((resolve, reject) => {
                var handle = setTimeout(() => reject(new exceptions.Timeout(msg)), ms);
                this.then(resolve, reject).finally(clearTimeout.bind(null, handle));
            }) : this;
    }
});
if (typeof Symbol !== 'undefined' && Symbol.toStringTag)
    setProp(DexiePromise.prototype, Symbol.toStringTag, 'Dexie.Promise');
globalPSD.env = snapShot();
function Listener(onFulfilled, onRejected, resolve, reject, zone) {
    this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
    this.onRejected = typeof onRejected === 'function' ? onRejected : null;
    this.resolve = resolve;
    this.reject = reject;
    this.psd = zone;
}
props(DexiePromise, {
    all: function () {
        var values = getArrayOf.apply(null, arguments)
            .map(onPossibleParallellAsync);
        return new DexiePromise(function (resolve, reject) {
            if (values.length === 0)
                return resolve([]);
            var remaining = values.length;
            values.forEach((a, i) => DexiePromise.resolve(a).then(x => {
                values[i] = x;
                if (!--remaining) {
                    resolve(values);
                }
            }, reject));
        });
    },
    resolve: value => {
        if (value instanceof DexiePromise)
            return value;
        if (value && typeof value.then === 'function')
            return new DexiePromise((resolve, reject) => {
                value.then(resolve, reject);
            });
        var rv = new DexiePromise(INTERNAL, true, value);
        linkToPreviousPromise(rv, currentFulfiller);
        return rv;
    },
    reject: PromiseReject,
    race: function () {
        var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
        return new DexiePromise((resolve, reject) => {
            values.map(value => DexiePromise.resolve(value).then(resolve, reject));
        });
    },
    PSD: {
        get: () => PSD,
        set: value => PSD = value
    },
    totalEchoes: { get: () => totalEchoes },
    newPSD: newScope,
    usePSD: usePSD,
    scheduler: {
        get: () => asap,
        set: value => { asap = value; }
    },
    rejectionMapper: {
        get: () => rejectionMapper,
        set: value => { rejectionMapper = value; }
    },
    follow: (fn, zoneProps) => {
        return new DexiePromise((resolve, reject) => {
            return newScope((resolve, reject) => {
                var psd = PSD;
                psd.unhandleds = [];
                psd.onunhandled = reject;
                psd.finalize = callBoth(function () {
                    run_at_end_of_this_or_next_physical_tick(() => {
                        this.unhandleds.length === 0 ? resolve() : reject(this.unhandleds[0]);
                    });
                }, psd.finalize);
                fn();
            }, zoneProps, resolve, reject);
        });
    }
});
if (NativePromise) {
    if (NativePromise.allSettled)
        setProp(DexiePromise, "allSettled", function () {
            const possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
            return new DexiePromise(resolve => {
                if (possiblePromises.length === 0)
                    return resolve([]);
                let remaining = possiblePromises.length;
                const results = new Array(remaining);
                possiblePromises.forEach((p, i) => DexiePromise.resolve(p).then(value => results[i] = { status: "fulfilled", value }, reason => results[i] = { status: "rejected", reason })
                    .then(() => --remaining || resolve(results)));
            });
        });
    if (NativePromise.any && typeof AggregateError !== 'undefined')
        setProp(DexiePromise, "any", function () {
            const possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync);
            return new DexiePromise((resolve, reject) => {
                if (possiblePromises.length === 0)
                    return reject(new AggregateError([]));
                let remaining = possiblePromises.length;
                const failures = new Array(remaining);
                possiblePromises.forEach((p, i) => DexiePromise.resolve(p).then(value => resolve(value), failure => {
                    failures[i] = failure;
                    if (!--remaining)
                        reject(new AggregateError(failures));
                }));
            });
        });
}
function executePromiseTask(promise, fn) {
    try {
        fn(value => {
            if (promise._state !== null)
                return;
            if (value === promise)
                throw new TypeError('A promise cannot be resolved with itself.');
            var shouldExecuteTick = promise._lib && beginMicroTickScope();
            if (value && typeof value.then === 'function') {
                executePromiseTask(promise, (resolve, reject) => {
                    value instanceof DexiePromise ?
                        value._then(resolve, reject) :
                        value.then(resolve, reject);
                });
            }
            else {
                promise._state = true;
                promise._value = value;
                propagateAllListeners(promise);
            }
            if (shouldExecuteTick)
                endMicroTickScope();
        }, ex => handleRejection(promise, ex));
    }
    catch (ex) {
        handleRejection(promise, ex);
    }
}
function handleRejection(promise, reason) {
    rejectingErrors.push(reason);
    if (promise._state !== null)
        return;
    var shouldExecuteTick = promise._lib && beginMicroTickScope();
    reason = rejectionMapper(reason);
    promise._state = false;
    promise._value = reason;
    debug && reason !== null && typeof reason === 'object' && !reason._promise && tryCatch(() => {
        var origProp = getPropertyDescriptor(reason, "stack");
        reason._promise = promise;
        setProp(reason, "stack", {
            get: () => stack_being_generated ?
                origProp && (origProp.get ?
                    origProp.get.apply(reason) :
                    origProp.value) :
                promise.stack
        });
    });
    addPossiblyUnhandledError(promise);
    propagateAllListeners(promise);
    if (shouldExecuteTick)
        endMicroTickScope();
}
function propagateAllListeners(promise) {
    var listeners = promise._listeners;
    promise._listeners = [];
    for (var i = 0, len = listeners.length; i < len; ++i) {
        propagateToListener(promise, listeners[i]);
    }
    var psd = promise._PSD;
    --psd.ref || psd.finalize();
    if (numScheduledCalls === 0) {
        ++numScheduledCalls;
        asap(() => {
            if (--numScheduledCalls === 0)
                finalizePhysicalTick();
        }, []);
    }
}
function propagateToListener(promise, listener) {
    if (promise._state === null) {
        promise._listeners.push(listener);
        return;
    }
    var cb = promise._state ? listener.onFulfilled : listener.onRejected;
    if (cb === null) {
        return (promise._state ? listener.resolve : listener.reject)(promise._value);
    }
    ++listener.psd.ref;
    ++numScheduledCalls;
    asap(callListener, [cb, promise, listener]);
}
function callListener(cb, promise, listener) {
    try {
        currentFulfiller = promise;
        var ret, value = promise._value;
        if (promise._state) {
            ret = cb(value);
        }
        else {
            if (rejectingErrors.length)
                rejectingErrors = [];
            ret = cb(value);
            if (rejectingErrors.indexOf(value) === -1)
                markErrorAsHandled(promise);
        }
        listener.resolve(ret);
    }
    catch (e) {
        listener.reject(e);
    }
    finally {
        currentFulfiller = null;
        if (--numScheduledCalls === 0)
            finalizePhysicalTick();
        --listener.psd.ref || listener.psd.finalize();
    }
}
function getStack(promise, stacks, limit) {
    if (stacks.length === limit)
        return stacks;
    var stack = "";
    if (promise._state === false) {
        var failure = promise._value, errorName, message;
        if (failure != null) {
            errorName = failure.name || "Error";
            message = failure.message || failure;
            stack = prettyStack(failure, 0);
        }
        else {
            errorName = failure;
            message = "";
        }
        stacks.push(errorName + (message ? ": " + message : "") + stack);
    }
    if (debug) {
        stack = prettyStack(promise._stackHolder, 2);
        if (stack && stacks.indexOf(stack) === -1)
            stacks.push(stack);
        if (promise._prev)
            getStack(promise._prev, stacks, limit);
    }
    return stacks;
}
function linkToPreviousPromise(promise, prev) {
    var numPrev = prev ? prev._numPrev + 1 : 0;
    if (numPrev < LONG_STACKS_CLIP_LIMIT) {
        promise._prev = prev;
        promise._numPrev = numPrev;
    }
}
function physicalTick() {
    beginMicroTickScope() && endMicroTickScope();
}
function beginMicroTickScope() {
    var wasRootExec = isOutsideMicroTick;
    isOutsideMicroTick = false;
    needsNewPhysicalTick = false;
    return wasRootExec;
}
function endMicroTickScope() {
    var callbacks, i, l;
    do {
        while (microtickQueue.length > 0) {
            callbacks = microtickQueue;
            microtickQueue = [];
            l = callbacks.length;
            for (i = 0; i < l; ++i) {
                var item = callbacks[i];
                item[0].apply(null, item[1]);
            }
        }
    } while (microtickQueue.length > 0);
    isOutsideMicroTick = true;
    needsNewPhysicalTick = true;
}
function finalizePhysicalTick() {
    var unhandledErrs = unhandledErrors;
    unhandledErrors = [];
    unhandledErrs.forEach(p => {
        p._PSD.onunhandled.call(null, p._value, p);
    });
    var finalizers = tickFinalizers.slice(0);
    var i = finalizers.length;
    while (i)
        finalizers[--i]();
}
function run_at_end_of_this_or_next_physical_tick(fn) {
    function finalizer() {
        fn();
        tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1);
    }
    tickFinalizers.push(finalizer);
    ++numScheduledCalls;
    asap(() => {
        if (--numScheduledCalls === 0)
            finalizePhysicalTick();
    }, []);
}
function addPossiblyUnhandledError(promise) {
    if (!unhandledErrors.some(p => p._value === promise._value))
        unhandledErrors.push(promise);
}
function markErrorAsHandled(promise) {
    var i = unhandledErrors.length;
    while (i)
        if (unhandledErrors[--i]._value === promise._value) {
            unhandledErrors.splice(i, 1);
            return;
        }
}
function PromiseReject(reason) {
    return new DexiePromise(INTERNAL, false, reason);
}
function wrap(fn, errorCatcher) {
    var psd = PSD;
    return function () {
        var wasRootExec = beginMicroTickScope(), outerScope = PSD;
        try {
            switchToZone(psd, true);
            return fn.apply(this, arguments);
        }
        catch (e) {
            errorCatcher && errorCatcher(e);
        }
        finally {
            switchToZone(outerScope, false);
            if (wasRootExec)
                endMicroTickScope();
        }
    };
}
const task = { awaits: 0, echoes: 0, id: 0 };
var taskCounter = 0;
var zoneStack = [];
var zoneEchoes = 0;
var totalEchoes = 0;
var zone_id_counter = 0;
function newScope(fn, props, a1, a2) {
    var parent = PSD, psd = Object.create(parent);
    psd.parent = parent;
    psd.ref = 0;
    psd.global = false;
    psd.id = ++zone_id_counter;
    var globalEnv = globalPSD.env;
    psd.env = patchGlobalPromise ? {
        Promise: DexiePromise,
        PromiseProp: { value: DexiePromise, configurable: true, writable: true },
        all: DexiePromise.all,
        race: DexiePromise.race,
        allSettled: DexiePromise.allSettled,
        any: DexiePromise.any,
        resolve: DexiePromise.resolve,
        reject: DexiePromise.reject,
        nthen: getPatchedPromiseThen(globalEnv.nthen, psd),
        gthen: getPatchedPromiseThen(globalEnv.gthen, psd)
    } : {};
    if (props)
        extend(psd, props);
    ++parent.ref;
    psd.finalize = function () {
        --this.parent.ref || this.parent.finalize();
    };
    var rv = usePSD(psd, fn, a1, a2);
    if (psd.ref === 0)
        psd.finalize();
    return rv;
}
function incrementExpectedAwaits() {
    if (!task.id)
        task.id = ++taskCounter;
    ++task.awaits;
    task.echoes += ZONE_ECHO_LIMIT;
    return task.id;
}
function decrementExpectedAwaits() {
    if (!task.awaits)
        return false;
    if (--task.awaits === 0)
        task.id = 0;
    task.echoes = task.awaits * ZONE_ECHO_LIMIT;
    return true;
}
function onPossibleParallellAsync(possiblePromise) {
    if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) {
        incrementExpectedAwaits();
        return possiblePromise.then(x => {
            decrementExpectedAwaits();
            return x;
        }, e => {
            decrementExpectedAwaits();
            return rejection(e);
        });
    }
    return possiblePromise;
}
function zoneEnterEcho(targetZone) {
    ++totalEchoes;
    if (!task.echoes || --task.echoes === 0) {
        task.echoes = task.id = 0;
    }
    zoneStack.push(PSD);
    switchToZone(targetZone, true);
}
function zoneLeaveEcho() {
    var zone = zoneStack[zoneStack.length - 1];
    zoneStack.pop();
    switchToZone(zone, false);
}
function switchToZone(targetZone, bEnteringZone) {
    var currentZone = PSD;
    if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) {
        enqueueNativeMicroTask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho);
    }
    if (targetZone === PSD)
        return;
    PSD = targetZone;
    if (currentZone === globalPSD)
        globalPSD.env = snapShot();
    if (patchGlobalPromise) {
        var GlobalPromise = globalPSD.env.Promise;
        var targetEnv = targetZone.env;
        nativePromiseProto.then = targetEnv.nthen;
        GlobalPromise.prototype.then = targetEnv.gthen;
        if (currentZone.global || targetZone.global) {
            Object.defineProperty(_global, 'Promise', targetEnv.PromiseProp);
            GlobalPromise.all = targetEnv.all;
            GlobalPromise.race = targetEnv.race;
            GlobalPromise.resolve = targetEnv.resolve;
            GlobalPromise.reject = targetEnv.reject;
            if (targetEnv.allSettled)
                GlobalPromise.allSettled = targetEnv.allSettled;
            if (targetEnv.any)
                GlobalPromise.any = targetEnv.any;
        }
    }
}
function snapShot() {
    var GlobalPromise = _global.Promise;
    return patchGlobalPromise ? {
        Promise: GlobalPromise,
        PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"),
        all: GlobalPromise.all,
        race: GlobalPromise.race,
        allSettled: GlobalPromise.allSettled,
        any: GlobalPromise.any,
        resolve: GlobalPromise.resolve,
        reject: GlobalPromise.reject,
        nthen: nativePromiseProto.then,
        gthen: GlobalPromise.prototype.then
    } : {};
}
function usePSD(psd, fn, a1, a2, a3) {
    var outerScope = PSD;
    try {
        switchToZone(psd, true);
        return fn(a1, a2, a3);
    }
    finally {
        switchToZone(outerScope, false);
    }
}
function enqueueNativeMicroTask(job) {
    queueMicrotask(job);
}
function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) {
    return typeof fn !== 'function' ? fn : function () {
        var outerZone = PSD;
        if (possibleAwait)
            incrementExpectedAwaits();
        switchToZone(zone, true);
        try {
            return fn.apply(this, arguments);
        }
        finally {
            switchToZone(outerZone, false);
            if (cleanup)
                enqueueNativeMicroTask(decrementExpectedAwaits);
        }
    };
}
function getPatchedPromiseThen(origThen, zone) {
    return function (onResolved, onRejected) {
        return origThen.call(this, nativeAwaitCompatibleWrap(onResolved, zone), nativeAwaitCompatibleWrap(onRejected, zone));
    };
}
const UNHANDLEDREJECTION = "unhandledrejection";
function globalError(err, promise) {
    var rv;
    try {
        rv = promise.onuncatched(err);
    }
    catch (e) { }
    if (rv !== false)
        try {
            var event, eventData = { promise: promise, reason: err };
            if (_global.document && document.createEvent) {
                event = document.createEvent('Event');
                event.initEvent(UNHANDLEDREJECTION, true, true);
                extend(event, eventData);
            }
            else if (_global.CustomEvent) {
                event = new CustomEvent(UNHANDLEDREJECTION, { detail: eventData });
                extend(event, eventData);
            }
            if (event && _global.dispatchEvent) {
                dispatchEvent(event);
                if (!_global.PromiseRejectionEvent && _global.onunhandledrejection)
                    try {
                        _global.onunhandledrejection(event);
                    }
                    catch (_) { }
            }
            if (debug && event && !event.defaultPrevented) {
                console.warn(`Unhandled rejection: ${err.stack || err}`);
            }
        }
        catch (e) { }
}
var rejection = DexiePromise.reject;

function tempTransaction(db, mode, storeNames, fn) {
    if (!db.idbdb || (!db._state.openComplete && (!PSD.letThrough && !db._vip))) {
        if (db._state.openComplete) {
            return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError));
        }
        if (!db._state.isBeingOpened) {
            if (!db._options.autoOpen)
                return rejection(new exceptions.DatabaseClosed());
            db.open().catch(nop);
        }
        return db._state.dbReadyPromise.then(() => tempTransaction(db, mode, storeNames, fn));
    }
    else {
        var trans = db._createTransaction(mode, storeNames, db._dbSchema);
        try {
            trans.create();
        }
        catch (ex) {
            return rejection(ex);
        }
        return trans._promise(mode, (resolve, reject) => {
            return newScope(() => {
                PSD.trans = trans;
                return fn(resolve, reject, trans);
            });
        }).then(result => {
            return trans._completion.then(() => result);
        });
    }
}

const DEXIE_VERSION = '3.2.0.meganz';
const maxString = String.fromCharCode(65535);
const minKey = -Infinity;
const INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array<string | number | Date>.";
const STRING_EXPECTED = "String expected.";
const connections = [];
const isIEOrEdge = typeof navigator !== 'undefined' && /(MSIE|Trident|Edge)/.test(navigator.userAgent);
const hasIEDeleteObjectStoreBug = isIEOrEdge;
const hangsOnDeleteLargeKeyRange = isIEOrEdge;
const dexieStackFrameFilter = frame => !/(dexie\.js|dexie\.min\.js)/.test(frame);
const DBNAMES_DB = '__dbnames';
const READONLY = 'readonly';
const READWRITE = 'readwrite';

function combine(filter1, filter2) {
    return filter1 ?
        filter2 ?
            function () { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } :
            filter1 :
        filter2;
}

const AnyRange = {
    type: 3 ,
    lower: -Infinity,
    lowerOpen: false,
    upper: [[]],
    upperOpen: false
};

function workaroundForUndefinedPrimKey(keyPath) {
    return typeof keyPath === "string" && !keyPath.includes('.')
        ? (obj) => {
            if (obj[keyPath] === undefined && (keyPath in obj)) {
                obj = deepClone(obj);
                delete obj[keyPath];
            }
            return obj;
        }
        : (obj) => obj;
}

function cmp(a, b) {
    try {
        const ta = type(a);
        const tb = type(b);
        if (ta !== tb) {
            if (ta === 'Array')
                return 1;
            if (tb === 'Array')
                return -1;
            if (ta === 'binary')
                return 1;
            if (tb === 'binary')
                return -1;
            if (ta === 'string')
                return 1;
            if (tb === 'string')
                return -1;
            if (ta === 'Date')
                return 1;
            if (tb !== 'Date')
                return NaN;
            return -1;
        }
        switch (ta) {
            case 'number':
            case 'Date':
            case 'string':
                return a > b ? 1 : a < b ? -1 : 0;
            case 'binary': {
                return compareUint8Arrays(getUint8Array(a), getUint8Array(b));
            }
            case 'Array':
                return compareArrays(a, b);
        }
    }
    catch (_a) { }
    return NaN;
}
function compareArrays(a, b) {
    const al = a.length;
    const bl = b.length;
    const l = al < bl ? al : bl;
    for (let i = 0; i < l; ++i) {
        const res = cmp(a[i], b[i]);
        if (res !== 0)
            return res;
    }
    return al === bl ? 0 : al < bl ? -1 : 1;
}
function compareUint8Arrays(a, b) {
    const al = a.length;
    const bl = b.length;
    const l = al < bl ? al : bl;
    for (let i = 0; i < l; ++i) {
        if (a[i] !== b[i])
            return a[i] < b[i] ? -1 : 1;
    }
    return al === bl ? 0 : al < bl ? -1 : 1;
}
function type(x) {
    const t = typeof x;
    if (t !== 'object')
        return t;
    if (ArrayBuffer.isView(x))
        return 'binary';
    const tsTag = toStringTag(x);
    return tsTag === 'ArrayBuffer' ? 'binary' : tsTag;
}
function getUint8Array(a) {
    if (a instanceof Uint8Array)
        return a;
    if (ArrayBuffer.isView(a))
        return new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
    return new Uint8Array(a);
}

class Table {
    _trans(mode, fn, writeLocked) {
        const trans = this._tx || PSD.trans;
        const tableName = this.name;
        function checkTableInTransaction(resolve, reject, trans) {
            if (!trans.schema[tableName])
                throw new exceptions.NotFound("Table " + tableName + " not part of transaction");
            return fn(trans.idbtrans, trans);
        }
        const wasRootExec = beginMicroTickScope();
        try {
            return trans && trans.db === this.db ?
                trans === PSD.trans ?
                    trans._promise(mode, checkTableInTransaction, writeLocked) :
                    newScope(() => trans._promise(mode, checkTableInTransaction, writeLocked), { trans: trans, transless: PSD.transless || PSD }) :
                tempTransaction(this.db, mode, [this.name], checkTableInTransaction);
        }
        finally {
            if (wasRootExec)
                endMicroTickScope();
        }
    }
    get(keyOrCrit, cb) {
        if (keyOrCrit && keyOrCrit.constructor === Object)
            return this.where(keyOrCrit).first(cb);
        return this._trans('readonly', (trans) => {
            return this.core.get({ trans, key: keyOrCrit })
                .then(res => this.hook.reading.fire(res));
        }).then(cb);
    }
    getKey(keyOrCrit, cb) {
        if (keyOrCrit && keyOrCrit.constructor === Object)
            return this.where(keyOrCrit).first(cb);
        return this._trans('readonly', (trans) => {
            return this.core.get({ trans, key: keyOrCrit, method: 'getKey' })
                .then(res => this.hook.reading.fire(res));
        }).then(cb);
    }
    getKeys(keys) {
        return this._trans('readonly', trans => {
            return this.core.getMany({
                keys,
                trans,
                method: 'getKey'
            }).then(result => result.map(res => this.hook.reading.fire(res)).filter(Boolean));
        });
    }
    exists(keys) {
        const a = isArray(keys);
        if (!a || keys.length == 1) {
            return this.getKey(a ? keys[0] : keys).then(res => res ? a ? [res] : res : false);
        }
        return this.getKeys(keys);
    }
    where(indexOrCrit) {
        if (typeof indexOrCrit === 'string')
            return new this.db.WhereClause(this, indexOrCrit);
        if (isArray(indexOrCrit))
            return new this.db.WhereClause(this, `[${indexOrCrit.join('+')}]`);
        const keyPaths = keys(indexOrCrit);
        if (keyPaths.length === 1)
            return this
                .where(keyPaths[0])
                .equals(indexOrCrit[keyPaths[0]]);
        const compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(ix => ix.compound &&
            keyPaths.every(keyPath => ix.keyPath.indexOf(keyPath) >= 0) &&
            ix.keyPath.every(keyPath => keyPaths.indexOf(keyPath) >= 0))[0];
        if (compoundIndex && this.db._maxKey !== maxString)
            return this
                .where(compoundIndex.name)
                .equals(compoundIndex.keyPath.map(kp => indexOrCrit[kp]));
        if (!compoundIndex && debug)
            console.warn(`The query ${JSON.stringify(indexOrCrit)} on ${this.name} would benefit of a ` +
                `compound index [${keyPaths.join('+')}]`);
        const { idxByName } = this.schema;
        function equals(a, b) {
            return cmp(a, b) === 0;
        }
        const [idx, filterFunction] = keyPaths.reduce(([prevIndex, prevFilterFn], keyPath) => {
            const index = idxByName[keyPath];
            const value = indexOrCrit[keyPath];
            return [
                prevIndex || index,
                prevIndex || !index ?
                    combine(prevFilterFn, index && index.multi ?
                        x => {
                            const prop = getByKeyPath(x, keyPath);
                            return isArray(prop) && prop.some(item => equals(value, item));
                        } : x => equals(value, getByKeyPath(x, keyPath)))
                    : prevFilterFn
            ];
        }, [null, null]);
        return idx ?
            this.where(idx.name).equals(indexOrCrit[idx.keyPath])
                .filter(filterFunction) :
            compoundIndex ?
                this.filter(filterFunction) :
                this.where(keyPaths).equals('');
    }
    filter(filterFunction) {
        return this.toCollection().and(filterFunction);
    }
    count(thenShortcut) {
        return this.toCollection().count(thenShortcut);
    }
    offset(offset) {
        return this.toCollection().offset(offset);
    }
    limit(numRows) {
        return this.toCollection().limit(numRows);
    }
    each(callback) {
        return this.toCollection().each(callback);
    }
    toArray(thenShortcut) {
        return this.toCollection().toArray(thenShortcut);
    }
    toCollection() {
        return new this.db.Collection(new this.db.WhereClause(this));
    }
    orderBy(index) {
        return new this.db.Collection(new this.db.WhereClause(this, isArray(index) ?
            `[${index.join('+')}]` :
            index));
    }
    reverse() {
        return this.toCollection().reverse();
    }
    mapToClass(constructor) {
        this.schema.mappedClass = constructor;
        const readHook = obj => {
            if (!obj)
                return obj;
            const res = Object.create(constructor.prototype);
            for (var m in obj)
                if (hasOwn(obj, m))
                    try {
                        res[m] = obj[m];
                    }
                    catch (_) { }
            return res;
        };
        if (this.schema.readHook) {
            this.hook.reading.unsubscribe(this.schema.readHook);
        }
        this.schema.readHook = readHook;
        this.hook("reading", readHook);
        return constructor;
    }
    defineClass() {
        function Class(content) {
            extend(this, content);
        }
        return this.mapToClass(Class);
    }
    add(obj, key) {
        const { auto, keyPath } = this.schema.primKey;
        let objToAdd = obj;
        if (keyPath && auto) {
            objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj);
        }
        return this._trans('readwrite', trans => {
            return this.core.mutate({ trans, type: 'add', keys: key != null ? [key] : null, values: [objToAdd] });
        }).then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult)
            .then(lastResult => {
            if (keyPath) {
                try {
                    setByKeyPath(obj, keyPath, lastResult);
                }
                catch (_) { }
            }
            return lastResult;
        });
    }
    update(keyOrObject, modifications) {
        if (typeof keyOrObject === 'object' && !isArray(keyOrObject)) {
            const key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath);
            if (key === undefined)
                return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key"));
            try {
                if (typeof modifications !== "function") {
                    keys(modifications).forEach(keyPath => {
                        setByKeyPath(keyOrObject, keyPath, modifications[keyPath]);
                    });
                }
                else {
                    modifications(keyOrObject, { value: keyOrObject, primKey: key });
                }
            }
            catch (_a) {
            }
            return this.where(":id").equals(key).modify(modifications);
        }
        else {
            return this.where(":id").equals(keyOrObject).modify(modifications);
        }
    }
    put(obj, key) {
        const { auto, keyPath } = this.schema.primKey;
        let objToAdd = obj;
        if (keyPath && auto) {
            objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj);
        }
        return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'put', values: [objToAdd], keys: key != null ? [key] : null }))
            .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult)
            .then(lastResult => {
            if (keyPath) {
                try {
                    setByKeyPath(obj, keyPath, lastResult);
                }
                catch (_) { }
            }
            return lastResult;
        });
    }
    delete(key) {
        return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'delete', keys: [key] }))
            .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined);
    }
    clear() {
        return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'deleteRange', range: AnyRange }))
            .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined);
    }
    bulkGet(keys) {
        return this._trans('readonly', trans => {
            return this.core.getMany({
                keys,
                trans
            }).then(result => result.map(res => this.hook.reading.fire(res)));
        });
    }
    bulkAdd(objects, keysOrOptions, options) {
        const keys = keysOrOptions && Array.isArray(keysOrOptions) ? keysOrOptions : undefined;
        options = options || (keys ? undefined : keysOrOptions);
        const wantResults = options ? options.allKeys : undefined;
        return this._trans('readwrite', trans => {
            const { auto, keyPath } = this.schema.primKey;
            if (keyPath && keys)
                throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys");
            if (keys && keys.length !== objects.length)
                throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length");
            const numObjects = objects.length;
            let objectsToAdd = keyPath && auto ?
                objects.map(workaroundForUndefinedPrimKey(keyPath)) :
                objects;
            return this.core.mutate({ trans, type: 'add', keys: keys, values: objectsToAdd, wantResults })
                .then(({ numFailures, results, lastResult, failures }) => {
                const result = wantResults ? results : lastResult;
                if (numFailures === 0)
                    return result;
                throw new BulkError(`${this.name}.bulkAdd(): ${numFailures} of ${numObjects} operations failed`, failures);
            });
        });
    }
    bulkPut(objects, keysOrOptions, options) {
        const keys = keysOrOptions && Array.isArray(keysOrOptions) ? keysOrOptions : undefined;
        options = options || (keys ? undefined : keysOrOptions);
        const wantResults = options ? options.allKeys : undefined;
        return this._trans('readwrite', trans => {
            const { auto, keyPath } = this.schema.primKey;
            if (keyPath && keys)
                throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys");
            if (keys && keys.length !== objects.length)
                throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length");
            const numObjects = objects.length;
            let objectsToPut = keyPath && auto ?
                objects.map(workaroundForUndefinedPrimKey(keyPath)) :
                objects;
            return this.core.mutate({ trans, type: 'put', keys: keys, values: objectsToPut, wantResults })
                .then(({ numFailures, results, lastResult, failures }) => {
                const result = wantResults ? results : lastResult;
                if (numFailures === 0)
                    return result;
                throw new BulkError(`${this.name}.bulkPut(): ${numFailures} of ${numObjects} operations failed`, failures);
            });
        });
    }
    bulkDelete(keys) {
        const numKeys = keys.length;
        return this._trans('readwrite', trans => {
            return this.core.mutate({ trans, type: 'delete', keys: keys });
        }).then(({ numFailures, lastResult, failures }) => {
            if (numFailures === 0)
                return lastResult;
            throw new BulkError(`${this.name}.bulkDelete(): ${numFailures} of ${numKeys} operations failed`, failures);
        });
    }
}

function Events(ctx) {
    var evs = {};
    var rv = function (eventName, subscriber) {
        if (subscriber) {
            var i = arguments.length, args = new Array(i - 1);
            while (--i)
                args[i - 1] = arguments[i];
            evs[eventName].subscribe.apply(null, args);
            return ctx;
        }
        else if (typeof (eventName) === 'string') {
            return evs[eventName];
        }
    };
    rv.addEventType = add;
    for (var i = 1, l = arguments.length; i < l; ++i) {
        add(arguments[i]);
    }
    return rv;
    function add(eventName, chainFunction, defaultFunction) {
        if (typeof eventName === 'object')
            return addConfiguredEvents(eventName);
        if (!chainFunction)
            chainFunction = reverseStoppableEventChain;
        if (!defaultFunction)
            defaultFunction = nop;
        var context = {
            subscribers: [],
            fire: defaultFunction,
            subscribe: function (cb) {
                if (context.subscribers.indexOf(cb) === -1) {
                    context.subscribers.push(cb);
                    context.fire = chainFunction(context.fire, cb);
                }
            },
            unsubscribe: function (cb) {
                context.subscribers = context.subscribers.filter(function (fn) { return fn !== cb; });
                context.fire = context.subscribers.reduce(chainFunction, defaultFunction);
            }
        };
        evs[eventName] = rv[eventName] = context;
        return context;
    }
    function addConfiguredEvents(cfg) {
        keys(cfg).forEach(function (eventName) {
            var args = cfg[eventName];
            if (isArray(args)) {
                add(eventName, cfg[eventName][0], cfg[eventName][1]);
            }
            else if (args === 'asap') {
                var context = add(eventName, mirror, function fire() {
                    var i = arguments.length, args = new Array(i);
                    while (i--)
                        args[i] = arguments[i];
                    context.subscribers.forEach(function (fn) {
                        asap$1(function fireEvent() {
                            fn.apply(null, args);
                        });
                    });
                });
            }
            else
                throw new exceptions.InvalidArgument("Invalid event config");
        });
    }
}

function makeClassConstructor(prototype, constructor) {
    derive(constructor).from({ prototype });
    return constructor;
}

function createTableConstructor(db) {
    return makeClassConstructor(Table.prototype, function Table(name, tableSchema, trans) {
        this.db = db;
        this._tx = trans;
        this.name = name;
        this.schema = tableSchema;
        this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, {
            "creating": [hookCreatingChain, nop],
            "reading": [pureFunctionChain, mirror],
            "updating": [hookUpdatingChain, nop],
            "deleting": [hookDeletingChain, nop]
        });
    });
}

function isPlainKeyRange(ctx, ignoreLimitFilter) {
    return !(ctx.filter || ctx.algorithm || ctx.or) &&
        (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter);
}
function addFilter(ctx, fn) {
    ctx.filter = combine(ctx.filter, fn);
}
function addReplayFilter(ctx, factory, isLimitFilter) {
    var curr = ctx.replayFilter;
    ctx.replayFilter = curr ? () => combine(curr(), factory()) : factory;
    ctx.justLimit = isLimitFilter && !curr;
}
function addMatchFilter(ctx, fn) {
    ctx.isMatch = combine(ctx.isMatch, fn);
}
function getIndexOrStore(ctx, coreSchema) {
    if (ctx.isPrimKey)
        return coreSchema.primaryKey;
    const index = coreSchema.getIndexByKeyPath(ctx.index);
    if (!index)
        throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed");
    return index;
}
function openCursor(ctx, coreTable, trans) {
    const index = getIndexOrStore(ctx, coreTable.schema);
    return coreTable.openCursor({
        trans,
        values: !ctx.keysOnly,
        reverse: ctx.dir === 'prev',
        unique: !!ctx.unique,
        query: {
            index,
            range: ctx.range
        }
    });
}
function iter(ctx, fn, coreTrans, coreTable) {
    const filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter;
    if (!ctx.or) {
        return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper);
    }
    else {
        const set = {};
        const union = (item, cursor, advance) => {
            if (!filter || filter(cursor, advance, result => cursor.stop(result), err => cursor.fail(err))) {
                var primaryKey = cursor.primaryKey;
                var key = '' + primaryKey;
                if (key === '[object ArrayBuffer]')
                    key = '' + new Uint8Array(primaryKey);
                if (!hasOwn(set, key)) {
                    set[key] = true;
                    fn(item, cursor, advance);
                }
            }
        };
        return Promise.all([
            ctx.or._iterate(union, coreTrans),
            iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper)
        ]);
    }
}
function iterate(cursorPromise, filter, fn, valueMapper) {
    var mappedFn = valueMapper ? (x, c, a) => fn(valueMapper(x), c, a) : fn;
    var wrappedFn = wrap(mappedFn);
    return cursorPromise.then(cursor => {
        if (cursor) {
            return cursor.start(() => {
                var c = () => cursor.continue();
                if (!filter || filter(cursor, advancer => c = advancer, val => { cursor.stop(val); c = nop; }, e => { cursor.fail(e); c = nop; }))
                    wrappedFn(cursor.value, cursor, advancer => c = advancer);
                c();
            });
        }
    });
}

class Collection {
    _read(fn, cb) {
        var ctx = this._ctx;
        return ctx.error ?
            ctx.table._trans(null, rejection.bind(null, ctx.error)) :
            ctx.table._trans('readonly', fn).then(cb);
    }
    _write(fn) {
        var ctx = this._ctx;
        return ctx.error ?
            ctx.table._trans(null, rejection.bind(null, ctx.error)) :
            ctx.table._trans('readwrite', fn, "locked");
    }
    _addAlgorithm(fn) {
        var ctx = this._ctx;
        ctx.algorithm = combine(ctx.algorithm, fn);
    }
    _iterate(fn, coreTrans) {
        return iter(this._ctx, fn, coreTrans, this._ctx.table.core);
    }
    clone(props) {
        var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx);
        if (props)
            extend(ctx, props);
        rv._ctx = ctx;
        return rv;
    }
    raw() {
        this._ctx.valueMapper = null;
        return this;
    }
    each(fn) {
        var ctx = this._ctx;
        return this._read(trans => iter(ctx, fn, trans, ctx.table.core));
    }
    count(cb) {
        return this._read(trans => {
            const ctx = this._ctx;
            const coreTable = ctx.table.core;
            if (isPlainKeyRange(ctx, true)) {
                return coreTable.count({
                    trans,
                    query: {
                        index: getIndexOrStore(ctx, coreTable.schema),
                        range: ctx.range
                    }
                }).then(count => Math.min(count, ctx.limit));
            }
            else {
                var count = 0;
                return iter(ctx, () => { ++count; return false; }, trans, coreTable)
                    .then(() => count);
            }
        }).then(cb);
    }
    sortBy(keyPath, cb) {
        const parts = keyPath.split('.').reverse(), lastPart = parts[0], lastIndex = parts.length - 1;
        function getval(obj, i) {
            if (i)
                return getval(obj[parts[i]], i - 1);
            return obj[lastPart];
        }
        var order = this._ctx.dir === "next" ? 1 : -1;
        function sorter(a, b) {
            var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex);
            return aVal < bVal ? -order : aVal > bVal ? order : 0;
        }
        return this.toArray(function (a) {
            return a.sort(sorter);
        }).then(cb);
    }
    toArray(cb) {
        return this._read(trans => {
            var ctx = this._ctx;
            if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) {
                const { valueMapper } = ctx;
                const index = getIndexOrStore(ctx, ctx.table.core.schema);
                return ctx.table.core.query({
                    trans,
                    limit: ctx.limit,
                    values: true,
                    query: {
                        index,
                        range: ctx.range
                    }
                }).then(({ result }) => valueMapper ? result.map(valueMapper) : result);
            }
            else {
                const a = [];
                return iter(ctx, item => a.push(item), trans, ctx.table.core).then(() => a);
            }
        }, cb);
    }
    offset(offset) {
        var ctx = this._ctx;
        if (offset <= 0)
            return this;
        ctx.offset += offset;
        if (isPlainKeyRange(ctx)) {
            addReplayFilter(ctx, () => {
                var offsetLeft = offset;
                return (cursor, advance) => {
                    if (offsetLeft === 0)
                        return true;
                    if (offsetLeft === 1) {
                        --offsetLeft;
                        return false;
                    }
                    advance(() => {
                        cursor.advance(offsetLeft);
                        offsetLeft = 0;
                    });
                    return false;
                };
            });
        }
        else {
            addReplayFilter(ctx, () => {
                var offsetLeft = offset;
                return () => (--offsetLeft < 0);
            });
        }
        return this;
    }
    limit(numRows) {
        this._ctx.limit = Math.min(this._ctx.limit, numRows);
        addReplayFilter(this._ctx, () => {
            var rowsLeft = numRows;
            return function (cursor, advance, resolve) {
                if (--rowsLeft <= 0)
                    advance(resolve);
                return rowsLeft >= 0;
            };
        }, true);
        return this;
    }
    until(filterFunction, bIncludeStopEntry) {
        addFilter(this._ctx, function (cursor, advance, resolve) {
            if (filterFunction(cursor.value)) {
                advance(resolve);
                return bIncludeStopEntry;
            }
            else {
                return true;
            }
        });
        return this;
    }
    first(cb) {
        return this.limit(1).toArray(function (a) { return a[0]; }).then(cb);
    }
    last(cb) {
        return this.reverse().first(cb);
    }
    filter(filterFunction) {
        addFilter(this._ctx, function (cursor) {
            return filterFunction(cursor.value);
        });
        addMatchFilter(this._ctx, filterFunction);
        return this;
    }
    and(filter) {
        return this.filter(filter);
    }
    or(indexName) {
        return new this.db.WhereClause(this._ctx.table, indexName, this);
    }
    reverse() {
        this._ctx.dir = (this._ctx.dir === "prev" ? "next" : "prev");
        if (this._ondirectionchange)
            this._ondirectionchange(this._ctx.dir);
        return this;
    }
    desc() {
        return this.reverse();
    }
    eachKey(cb) {
        var ctx = this._ctx;
        ctx.keysOnly = !ctx.isMatch;
        return this.each(function (val, cursor) { cb(cursor.key, cursor); });
    }
    eachUniqueKey(cb) {
        this._ctx.unique = "unique";
        return this.eachKey(cb);
    }
    eachPrimaryKey(cb) {
        var ctx = this._ctx;
        ctx.keysOnly = !ctx.isMatch;
        return this.each(function (val, cursor) { cb(cursor.primaryKey, cursor); });
    }
    keys(cb) {
        var ctx = this._ctx;
        ctx.keysOnly = !ctx.isMatch;
        var a = [];
        return this.each(function (item, cursor) {
            a.push(cursor.key);
        }).then(function () {
            return a;
        }).then(cb);
    }
    primaryKeys(cb) {
        var ctx = this._ctx;
        if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) {
            return this._read(trans => {
                var index = getIndexOrStore(ctx, ctx.table.core.schema);
                return ctx.table.core.query({
                    trans,
                    values: false,
                    limit: ctx.limit,
                    query: {
                        index,
                        range: ctx.range
                    }
                });
            }).then(({ result }) => result).then(cb);
        }
        ctx.keysOnly = !ctx.isMatch;
        var a = [];
        return this.each(function (item, cursor) {
            a.push(cursor.primaryKey);
        }).then(function () {
            return a;
        }).then(cb);
    }
    uniqueKeys(cb) {
        this._ctx.unique = "unique";
        return this.keys(cb);
    }
    firstKey(cb) {
        return this.limit(1).keys(function (a) { return a[0]; }).then(cb);
    }
    lastKey(cb) {
        return this.reverse().firstKey(cb);
    }
    distinct() {
        var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index];
        if (!idx || !idx.multi)
            return this;
        var set = {};
        addFilter(this._ctx, function (cursor) {
            var strKey = cursor.primaryKey.toString();
            var found = hasOwn(set, strKey);
            set[strKey] = true;
            return !found;
        });
        return this;
    }
    modify(changes) {
        var ctx = this._ctx;
        return this._write(trans => {
            var modifyer;
            if (typeof changes === 'function') {
                modifyer = changes;
            }
            else {
                var keyPaths = keys(changes);
                var numKeys = keyPaths.length;
                modifyer = function (item) {
                    var anythingModified = false;
                    for (var i = 0; i < numKeys; ++i) {
                        var keyPath = keyPaths[i], val = changes[keyPath];
                        if (getByKeyPath(item, keyPath) !== val) {
                            setByKeyPath(item, keyPath, val);
                            anythingModified = true;
                        }
                    }
                    return anythingModified;
                };
            }
            const coreTable = ctx.table.core;
            const { outbound, extractKey } = coreTable.schema.primaryKey;
            const limit = this.db._options.modifyChunkSize || 200;
            const totalFailures = [];
            let successCount = 0;
            const failedKeys = [];
            const applyMutateResult = (expectedCount, res) => {
                const { failures, numFailures } = res;
                successCount += expectedCount - numFailures;
                for (let pos of keys(failures)) {
                    totalFailures.push(failures[pos]);
                }
            };
            return this.clone().primaryKeys().then(keys => {
                const nextChunk = (offset) => {
                    const count = Math.min(limit, keys.length - offset);
                    return coreTable.getMany({
                        trans,
                        keys: keys.slice(offset, offset + count),
                        cache: "immutable"
                    }).then(values => {
                        const addValues = [];
                        const putValues = [];
                        const putKeys = outbound ? [] : null;
                        const deleteKeys = [];
                        for (let i = 0; i < count; ++i) {
                            const origValue = values[i];
                            const ctx = {
                                value: deepClone(origValue),
                                primKey: keys[offset + i]
                            };
                            if (modifyer.call(ctx, ctx.value, ctx) !== false) {
                                if (ctx.value == null) {
                                    deleteKeys.push(keys[offset + i]);
                                }
                                else if (!outbound && cmp(extractKey(origValue), extractKey(ctx.value)) !== 0) {
                                    deleteKeys.push(keys[offset + i]);
                                    addValues.push(ctx.value);
                                }
                                else {
                                    putValues.push(ctx.value);
                                    if (outbound)
                                        putKeys.push(keys[offset + i]);
                                }
                            }
                        }
                        const criteria = isPlainKeyRange(ctx) &&
                            ctx.limit === Infinity &&
                            (typeof changes !== 'function' || changes === deleteCallback) && {
                            index: ctx.index,
                            range: ctx.range
                        };
                        return Promise.resolve(addValues.length > 0 &&
                            coreTable.mutate({ trans, type: 'add', values: addValues })
                                .then(res => {
                                for (let pos in res.failures) {
                                    deleteKeys.splice(parseInt(pos), 1);
                                }
                                applyMutateResult(addValues.length, res);
                            })).then(() => (putValues.length > 0 || (criteria && typeof changes === 'object')) &&
                            coreTable.mutate({
                                trans,
                                type: 'put',
                                keys: putKeys,
                                values: putValues,
                                criteria,
                                changeSpec: typeof changes !== 'function'
                                    && changes
                            }).then(res => applyMutateResult(putValues.length, res))).then(() => (deleteKeys.length > 0 || (criteria && changes === deleteCallback)) &&
                            coreTable.mutate({
                                trans,
                                type: 'delete',
                                keys: deleteKeys,
                                criteria
                            }).then(res => applyMutateResult(deleteKeys.length, res))).then(() => {
                            return keys.length > offset + count && nextChunk(offset + limit);
                        });
                    });
                };
                return nextChunk(0).then(() => {
                    if (totalFailures.length > 0)
                        throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys);
                    return keys.length;
                });
            });
        });
    }
    delete() {
        var ctx = this._ctx, range = ctx.range;
        if (isPlainKeyRange(ctx) &&
            ((ctx.isPrimKey && !hangsOnDeleteLargeKeyRange) || range.type === 3 ))
         {
            return this._write(trans => {
                const { primaryKey } = ctx.table.core.schema;
                const coreRange = range;
                return ctx.table.core.count({ trans, query: { index: primaryKey, range: coreRange } }).then(count => {
                    return ctx.table.core.mutate({ trans, type: 'deleteRange', range: coreRange })
                        .then(({ failures, lastResult, results, numFailures }) => {
                        if (numFailures)
                            throw new ModifyError("Could not delete some values", Object.keys(failures).map(pos => failures[pos]), count - numFailures);
                        return count - numFailures;
                    });
                });
            });
        }
        return this.modify(deleteCallback);
    }
}
const deleteCallback = (value, ctx) => ctx.value = null;

function createCollectionConstructor(db) {
    return makeClassConstructor(Collection.prototype, function Collection(whereClause, keyRangeGenerator) {
        this.db = db;
        let keyRange = AnyRange, error = null;
        if (keyRangeGenerator)
            try {
                keyRange = keyRangeGenerator();
            }
            catch (ex) {
                error = ex;
            }
        const whereCtx = whereClause._ctx;
        const table = whereCtx.table;
        const readingHook = table.hook.reading.fire;
        this._ctx = {
            table: table,
            index: whereCtx.index,
            isPrimKey: (!whereCtx.index || (table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name)),
            range: keyRange,
            keysOnly: false,
            dir: "next",
            unique: "",
            algorithm: null,
            filter: null,
            replayFilter: null,
            justLimit: true,
            isMatch: null,
            offset: 0,
            limit: Infinity,
            error: error,
            or: whereCtx.or,
            valueMapper: readingHook !== mirror ? readingHook : null
        };
    });
}

function simpleCompare(a, b) {
    return a < b ? -1 : a === b ? 0 : 1;
}
function simpleCompareReverse(a, b) {
    return a > b ? -1 : a === b ? 0 : 1;
}

function fail(collectionOrWhereClause, err, T) {
    var collection = collectionOrWhereClause instanceof WhereClause ?
        new collectionOrWhereClause.Collection(collectionOrWhereClause) :
        collectionOrWhereClause;
    collection._ctx.error = T ? new T(err) : new TypeError(err);
    return collection;
}
function emptyCollection(whereClause) {
    return new whereClause.Collection(whereClause, () => rangeEqual("")).limit(0);
}
function upperFactory(dir) {
    return dir === "next" ?
        (s) => s.toUpperCase() :
        (s) => s.toLowerCase();
}
function lowerFactory(dir) {
    return dir === "next" ?
        (s) => s.toLowerCase() :
        (s) => s.toUpperCase();
}
function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp, dir) {
    var length = Math.min(key.length, lowerNeedle.length);
    var llp = -1;
    for (var i = 0; i < length; ++i) {
        var lwrKeyChar = lowerKey[i];
        if (lwrKeyChar !== lowerNeedle[i]) {
            if (cmp(key[i], upperNeedle[i]) < 0)
                return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1);
            if (cmp(key[i], lowerNeedle[i]) < 0)
                return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1);
            if (llp >= 0)
                return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1);
            return null;
        }
        if (cmp(key[i], lwrKeyChar) < 0)
            llp = i;
    }
    if (length < lowerNeedle.length && dir === "next")
        return key + upperNeedle.substr(key.length);
    if (length < key.length && dir === "prev")
        return key.substr(0, upperNeedle.length);
    return (llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1));
}
function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) {
    var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length;
    if (!needles.every(s => typeof s === 'string')) {
        return fail(whereClause, STRING_EXPECTED);
    }
    function initDirection(dir) {
        upper = upperFactory(dir);
        lower = lowerFactory(dir);
        compare = (dir === "next" ? simpleCompare : simpleCompareReverse);
        var needleBounds = needles.map(function (needle) {
            return { lower: lower(needle), upper: upper(needle) };
        }).sort(function (a, b) {
            return compare(a.lower, b.lower);
        });
        upperNeedles = needleBounds.map(function (nb) { return nb.upper; });
        lowerNeedles = needleBounds.map(function (nb) { return nb.lower; });
        direction = dir;
        nextKeySuffix = (dir === "next" ? "" : suffix);
    }
    initDirection("next");
    var c = new whereClause.Collection(whereClause, () => createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix));
    c._ondirectionchange = function (direction) {
        initDirection(direction);
    };
    var firstPossibleNeedle = 0;
    c._addAlgorithm(function (cursor, advance, resolve) {
        var key = cursor.key;
        if (typeof key !== 'string')
            return false;
        var lowerKey = lower(key);
        if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) {
            return true;
        }
        else {
            var lowestPossibleCasing = null;
            for (var i = firstPossibleNeedle; i < needlesLen; ++i) {
                var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction);
                if (casing === null && lowestPossibleCasing === null)
                    firstPossibleNeedle = i + 1;
                else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) {
                    lowestPossibleCasing = casing;
                }
            }
            if (lowestPossibleCasing !== null) {
                advance(function () { cursor.continue(lowestPossibleCasing + nextKeySuffix); });
            }
            else {
                advance(resolve);
            }
            return false;
        }
    });
    return c;
}
function createRange(lower, upper, lowerOpen, upperOpen) {
    return {
        type: 2 ,
        lower,
        upper,
        lowerOpen,
        upperOpen
    };
}
function rangeEqual(value) {
    return {
        type: 1 ,
        lower: value,
        upper: value
    };
}

class WhereClause {
    get Collection() {
        return this._ctx.table.db.Collection;
    }
    between(lower, upper, includeLower, includeUpper) {
        includeLower = includeLower !== false;
        includeUpper = includeUpper === true;
        try {
            if ((this._cmp(lower, upper) > 0) ||
                (this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper)))
                return emptyCollection(this);
            return new this.Collection(this, () => createRange(lower, upper, !includeLower, !includeUpper));
        }
        catch (e) {
            return fail(this, INVALID_KEY_ARGUMENT);
        }
    }
    equals(value) {
        if (value == null)
            return fail(this, INVALID_KEY_ARGUMENT);
        return new this.Collection(this, () => rangeEqual(value));
    }
    above(value) {
        if (value == null)
            return fail(this, INVALID_KEY_ARGUMENT);
        return new this.Collection(this, () => createRange(value, undefined, true));
    }
    aboveOrEqual(value) {
        if (value == null)
            return fail(this, INVALID_KEY_ARGUMENT);
        return new this.Collection(this, () => createRange(value, undefined, false));
    }
    below(value) {
        if (value == null)
            return fail(this, INVALID_KEY_ARGUMENT);
        return new this.Collection(this, () => createRange(undefined, value, false, true));
    }
    belowOrEqual(value) {
        if (value == null)
            return fail(this, INVALID_KEY_ARGUMENT);
        return new this.Collection(this, () => createRange(undefined, value));
    }
    startsWith(str) {
        if (typeof str !== 'string')
            return fail(this, STRING_EXPECTED);
        return this.between(str, str + maxString, true, true);
    }
    startsWithIgnoreCase(str) {
        if (str === "")
            return this.startsWith(str);
        return addIgnoreCaseAlgorithm(this, (x, a) => x.indexOf(a[0]) === 0, [str], maxString);
    }
    equalsIgnoreCase(str) {
        return addIgnoreCaseAlgorithm(this, (x, a) => x === a[0], [str], "");
    }
    anyOfIgnoreCase() {
        var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
        if (set.length === 0)
            return emptyCollection(this);
        return addIgnoreCaseAlgorithm(this, (x, a) => a.indexOf(x) !== -1, set, "");
    }
    startsWithAnyOfIgnoreCase() {
        var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
        if (set.length === 0)
            return emptyCollection(this);
        return addIgnoreCaseAlgorithm(this, (x, a) => a.some(n => x.indexOf(n) === 0), set, maxString);
    }
    anyOf() {
        const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
        let compare = this._cmp;
        try {
            set.sort(compare);
        }
        catch (e) {
            return fail(this, INVALID_KEY_ARGUMENT);
        }
        if (set.length === 0)
            return emptyCollection(this);
        const c = new this.Collection(this, () => createRange(set[0], set[set.length - 1]));
        c._ondirectionchange = direction => {
            compare = (direction === "next" ?
                this._ascending :
                this._descending);
            set.sort(compare);
        };
        let i = 0;
        c._addAlgorithm((cursor, advance, resolve) => {
            const key = cursor.key;
            while (compare(key, set[i]) > 0) {
                ++i;
                if (i === set.length) {
                    advance(resolve);
                    return false;
                }
            }
            if (compare(key, set[i]) === 0) {
                return true;
            }
            else {
                advance(() => { cursor.continue(set[i]); });
                return false;
            }
        });
        return c;
    }
    notEqual(value) {
        return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false });
    }
    noneOf() {
        const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
        if (set.length === 0)
            return new this.Collection(this);
        try {
            set.sort(this._ascending);
        }
        catch (e) {
            return fail(this, INVALID_KEY_ARGUMENT);
        }
        const ranges = set.reduce((res, val) => res ?
            res.concat([[res[res.length - 1][1], val]]) :
            [[minKey, val]], null);
        ranges.push([set[set.length - 1], this.db._maxKey]);
        return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false });
    }
    inAnyRange(ranges, options) {
        const cmp = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max;
        if (ranges.length === 0)
            return emptyCollection(this);
        if (!ranges.every(range => range[0] !== undefined &&
            range[1] !== undefined &&
            ascending(range[0], range[1]) <= 0)) {
            return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument);
        }
        const includeLowers = !options || options.includeLowers !== false;
        const includeUppers = options && options.includeUppers === true;
        function addRange(ranges, newRange) {
            let i = 0, l = ranges.length;
            for (; i < l; ++i) {
                const range = ranges[i];
                if (cmp(newRange[0], range[1]) < 0 && cmp(newRange[1], range[0]) > 0) {
                    range[0] = min(range[0], newRange[0]);
                    range[1] = max(range[1], newRange[1]);
                    break;
                }
            }
            if (i === l)
                ranges.push(newRange);
            return ranges;
        }
        let sortDirection = ascending;
        function rangeSorter(a, b) { return sortDirection(a[0], b[0]); }
        let set;
        try {
            set = ranges.reduce(addRange, []);
            set.sort(rangeSorter);
        }
        catch (ex) {
            return fail(this, INVALID_KEY_ARGUMENT);
        }
        let rangePos = 0;
        const keyIsBeyondCurrentEntry = includeUppers ?
            key => ascending(key, set[rangePos][1]) > 0 :
            key => ascending(key, set[rangePos][1]) >= 0;
        const keyIsBeforeCurrentEntry = includeLowers ?
            key => descending(key, set[rangePos][0]) > 0 :
            key => descending(key, set[rangePos][0]) >= 0;
        function keyWithinCurrentRange(key) {
            return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key);
        }
        let checkKey = keyIsBeyondCurrentEntry;
        const c = new this.Collection(this, () => createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers));
        c._ondirectionchange = direction => {
            if (direction === "next") {
                checkKey = keyIsBeyondCurrentEntry;
                sortDirection = ascending;
            }
            else {
                checkKey = keyIsBeforeCurrentEntry;
                sortDirection = descending;
            }
            set.sort(rangeSorter);
        };
        c._addAlgorithm((cursor, advance, resolve) => {
            var key = cursor.key;
            while (checkKey(key)) {
                ++rangePos;
                if (rangePos === set.length) {
                    advance(resolve);
                    return false;
                }
            }
            if (keyWithinCurrentRange(key)) {
                return true;
            }
            else if (this._cmp(key, set[rangePos][1]) === 0 || this._cmp(key, set[rangePos][0]) === 0) {
                return false;
            }
            else {
                advance(() => {
                    if (sortDirection === ascending)
                        cursor.continue(set[rangePos][0]);
                    else
                        cursor.continue(set[rangePos][1]);
                });
                return false;
            }
        });
        return c;
    }
    startsWithAnyOf() {
        const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments);
        if (!set.every(s => typeof s === 'string')) {
            return fail(this, "startsWithAnyOf() only works with strings");
        }
        if (set.length === 0)
            return emptyCollection(this);
        return this.inAnyRange(set.map((str) => [str, str + maxString]));
    }
}

function createWhereClauseConstructor(db) {
    return makeClassConstructor(WhereClause.prototype, function WhereClause(table, index, orCollection) {
        this.db = db;
        this._ctx = {
            table: table,
            index: index === ":id" ? null : index,
            or: orCollection
        };
        this._cmp = this._ascending = cmp;
        this._descending = (a, b) => cmp(b, a);
        this._max = (a, b) => cmp(a, b) > 0 ? a : b;
        this._min = (a, b) => cmp(a, b) < 0 ? a : b;
        this._IDBKeyRange = db._deps.IDBKeyRange;
    });
}

function eventRejectHandler(reject) {
    return wrap(function (event) {
        preventDefault(event);
        reject(event.target.error);
        return false;
    });
}
function preventDefault(event) {
    if (event.stopPropagation)
        event.stopPropagation();
    if (event.preventDefault)
        event.preventDefault();
}

class Transaction {
    _lock() {
        assert(!PSD.global);
        ++this._reculock;
        if (this._reculock === 1 && !PSD.global)
            PSD.lockOwnerFor = this;
        return this;
    }
    _unlock() {
        assert(!PSD.global);
        if (--this._reculock === 0) {
            if (!PSD.global)
                PSD.lockOwnerFor = null;
            while (this._blockedFuncs.length > 0 && !this._locked()) {
                var fnAndPSD = this._blockedFuncs.shift();
                try {
                    usePSD(fnAndPSD[1], fnAndPSD[0]);
                }
                catch (e) { }
            }
        }
        return this;
    }
    _locked() {
        return this._reculock && PSD.lockOwnerFor !== this;
    }
    create(idbtrans) {
        if (!this.mode)
            return this;
        const idbdb = this.db.idbdb;
        const dbOpenError = this.db._state.dbOpenError;
        assert(!this.idbtrans);
        if (!idbtrans && !idbdb) {
            switch (dbOpenError && dbOpenError.name) {
                case "DatabaseClosedError":
                    throw new exceptions.DatabaseClosed(dbOpenError);
                case "MissingAPIError":
                    throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError);
                default:
                    throw new exceptions.OpenFailed(dbOpenError);
            }
        }
        if (!this.active)
            throw new exceptions.TransactionInactive();
        assert(this._completion._state === null);
        idbtrans = this.idbtrans = idbtrans ||
            (this.db.core
                ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })
                : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }));
        idbtrans.onerror = wrap(ev => {
            preventDefault(ev);
            this._reject(idbtrans.error);
        });
        idbtrans.onabort = wrap(ev => {
            preventDefault(ev);
            this.active && this._reject(new exceptions.Abort(idbtrans.error));
            this.active = false;
            this.on("abort").fire(ev);
        });
        idbtrans.oncomplete = wrap(() => {
            this.active = false;
            this._resolve();
        });
        return this;
    }
    _promise(mode, fn, bWriteLock) {
        if (mode === 'readwrite' && this.mode !== 'readwrite')
            return rejection(new exceptions.ReadOnly("Transaction is readonly"));
        if (!this.active)
            return rejection(new exceptions.TransactionInactive());
        if (this._locked()) {
            return new DexiePromise((resolve, reject) => {
                this._blockedFuncs.push([() => {
                        this._promise(mode, fn, bWriteLock).then(resolve, reject);
                    }, PSD]);
            });
        }
        else if (bWriteLock) {
            return newScope(() => {
                var p = new DexiePromise((resolve, reject) => {
                    this._lock();
                    const rv = fn(resolve, reject, this);
                    if (rv && rv.then)
                        rv.then(resolve, reject);
                });
                p.finally(() => this._unlock());
                p._lib = true;
                return p;
            });
        }
        else {
            var p = new DexiePromise((resolve, reject) => {
                var rv = fn(resolve, reject, this);
                if (rv && rv.then)
                    rv.then(resolve, reject);
            });
            p._lib = true;
            return p;
        }
    }
    _root() {
        return this.parent ? this.parent._root() : this;
    }
    waitFor(promiseLike) {
        var root = this._root();
        const promise = DexiePromise.resolve(promiseLike);
        if (root._waitingFor) {
            root._waitingFor = root._waitingFor.then(() => promise);
        }
        else {
            root._waitingFor = promise;
            root._waitingQueue = [];
            var store = root.idbtrans.objectStore(root.storeNames[0]);
            (function spin() {
                ++root._spinCount;
                while (root._waitingQueue.length)
                    (root._waitingQueue.shift())();
                if (root._waitingFor)
                    store.get(-Infinity).onsuccess = spin;
            }());
        }
        var currentWaitPromise = root._waitingFor;
        return new DexiePromise((resolve, reject) => {
            promise.then(res => root._waitingQueue.push(wrap(resolve.bind(null, res))), err => root._waitingQueue.push(wrap(reject.bind(null, err)))).finally(() => {
                if (root._waitingFor === currentWaitPromise) {
                    root._waitingFor = null;
                }
            });
        });
    }
    abort() {
        if (this.active) {
            this.active = false;
            if (this.idbtrans)
                this.idbtrans.abort();
            this._reject(new exceptions.Abort());
        }
    }
    table(tableName) {
        const memoizedTables = (this._memoizedTables || (this._memoizedTables = {}));
        if (hasOwn(memoizedTables, tableName))
            return memoizedTables[tableName];
        const tableSchema = this.schema[tableName];
        if (!tableSchema) {
            throw new exceptions.NotFound("Table " + tableName + " not part of transaction");
        }
        const transactionBoundTable = new this.db.Table(tableName, tableSchema, this);
        transactionBoundTable.core = this.db.core.table(tableName);
        memoizedTables[tableName] = transactionBoundTable;
        return transactionBoundTable;
    }
}

function createTransactionConstructor(db) {
    return makeClassConstructor(Transaction.prototype, function Transaction(mode, storeNames, dbschema, chromeTransactionDurability, parent) {
        this.db = db;
        this.mode = mode;
        this.storeNames = storeNames;
        this.schema = dbschema;
        this.chromeTransactionDurability = chromeTransactionDurability;
        this.idbtrans = null;
        this.on = Events(this, "complete", "error", "abort");
        this.parent = parent || null;
        this.active = true;
        this._reculock = 0;
        this._blockedFuncs = [];
        this._resolve = null;
        this._reject = null;
        this._waitingFor = null;
        this._waitingQueue = null;
        this._spinCount = 0;
        this._completion = new DexiePromise((resolve, reject) => {
            this._resolve = resolve;
            this._reject = reject;
        });
        this._completion.then(() => {
            this.active = false;
            this.on.complete.fire();
        }, e => {
            var wasActive = this.active;
            this.active = false;
            this.on.error.fire(e);
            this.parent ?
                this.parent._reject(e) :
                wasActive && this.idbtrans && this.idbtrans.abort();
            return rejection(e);
        });
    });
}

function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey) {
    return {
        name,
        keyPath,
        unique,
        multi,
        auto,
        compound,
        src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath)
    };
}
function nameFromKeyPath(keyPath) {
    return typeof keyPath === 'string' ?
        keyPath :
        keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : "";
}

function createTableSchema(name, primKey, indexes) {
    return {
        name,
        primKey,
        indexes,
        mappedClass: null,
        idxByName: arrayToObject(indexes, index => [index.name, index])
    };
}

function safariMultiStoreFix(storeNames) {
    return storeNames.length === 1 ? storeNames[0] : storeNames;
}
let getMaxKey = (IdbKeyRange) => {
    try {
        IdbKeyRange.only([[]]);
        getMaxKey = () => [[]];
        return [[]];
    }
    catch (e) {
        getMaxKey = () => maxString;
        return maxString;
    }
};

function getKeyExtractor(keyPath) {
    if (keyPath == null) {
        return () => undefined;
    }
    else if (typeof keyPath === 'string') {
        return getSinglePathKeyExtractor(keyPath);
    }
    else {
        return obj => getByKeyPath(obj, keyPath);
    }
}
function getSinglePathKeyExtractor(keyPath) {
    const split = keyPath.split('.');
    if (split.length === 1) {
        return obj => obj[keyPath];
    }
    else {
        return obj => getByKeyPath(obj, keyPath);
    }
}

function arrayify(arrayLike) {
    return [].slice.call(arrayLike);
}
let _id_counter = 0;
function getKeyPathAlias(keyPath) {
    return keyPath == null ?
        ":id" :
        typeof keyPath === 'string' ?
            keyPath :
            `[${keyPath.join('+')}]`;
}
function createDBCore(db, IdbKeyRange, tmpTrans) {
    function extractSchema(db, trans) {
        const tables = arrayify(db.objectStoreNames);
        return {
            schema: {
                name: db.name,
                tables: tables.map(table => trans.objectStore(table)).map(store => {
                    const { keyPath, autoIncrement } = store;
                    const compound = isArray(keyPath);
                    const outbound = keyPath == null;
                    const indexByKeyPath = {};
                    const result = {
                        name: store.name,
                        primaryKey: {
                            name: null,
                            isPrimaryKey: true,
                            outbound,
                            compound,
                            keyPath,
                            autoIncrement,
                            unique: true,
                            extractKey: getKeyExtractor(keyPath)
                        },
                        indexes: arrayify(store.indexNames).map(indexName => store.index(indexName))
                            .map(index => {
                            const { name, unique, multiEntry, keyPath } = index;
                            const compound = isArray(keyPath);
                            const result = {
                                name,
                                compound,
                                keyPath,
                                unique,
                                multiEntry,
                                extractKey: getKeyExtractor(keyPath)
                            };
                            indexByKeyPath[getKeyPathAlias(keyPath)] = result;
                            return result;
                        }),
                        getIndexByKeyPath: (keyPath) => indexByKeyPath[getKeyPathAlias(keyPath)]
                    };
                    indexByKeyPath[":id"] = result.primaryKey;
                    if (keyPath != null) {
                        indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey;
                    }
                    return result;
                })
            },
            hasGetAll: tables.length > 0 && ('getAll' in trans.objectStore(tables[0])) &&
                !(typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) &&
                    !/(Chrome\/|Edge\/)/.test(navigator.userAgent) &&
                    [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604)
        };
    }
    function makeIDBKeyRange(range) {
        if (range.type === 3 )
            return null;
        if (range.type === 4 )
            throw new Error("Cannot convert never type to IDBKeyRange");
        const { lower, upper, lowerOpen, upperOpen } = range;
        const idbRange = lower === undefined ?
            upper === undefined ?
                null :
                IdbKeyRange.upperBound(upper, !!upperOpen) :
            upper === undefined ?
                IdbKeyRange.lowerBound(lower, !!lowerOpen) :
                IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen);
        return idbRange;
    }
    function createDbCoreTable(tableSchema) {
        const tableName = tableSchema.name;
        function mutate({ trans, type, keys, values, range }) {
            return new Promise((resolve, reject) => {
                resolve = wrap(resolve);
                const store = trans.objectStore(tableName);
                const outbound = store.keyPath == null;
                const isAddOrPut = type === "put" || type === "add";
                if (!isAddOrPut && type !== 'delete' && type !== 'deleteRange')
                    throw new Error("Invalid operation type: " + type);
                const { length } = keys || values || { length: 1 };
                if (keys && values && keys.length !== values.length) {
                    throw new Error("Given keys array must have same length as given values array.");
                }
                if (length === 0)
                    return resolve({ numFailures: 0, failures: {}, results: [], lastResult: undefined });
                let req;
                const reqs = [];
                const failures = [];
                let numFailures = 0;
                const errorHandler = event => {
                    ++numFailures;
                    preventDefault(event);
                };
                if (type === 'deleteRange') {
                    if (range.type === 4 )
                        return resolve({ numFailures, failures, results: [], lastResult: undefined });
                    if (range.type === 3 )
                        reqs.push(req = store.clear());
                    else
                        reqs.push(req = store.delete(makeIDBKeyRange(range)));
                }
                else {
                    const [args1, args2] = isAddOrPut ?
                        outbound ?
                            [values, keys] :
                            [values, null] :
                        [keys, null];
                    if (isAddOrPut) {
                        for (let i = 0; i < length; ++i) {
                            reqs.push(req = (args2 && args2[i] !== undefined ?
                                store[type](args1[i], args2[i]) :
                                store[type](args1[i])));
                            req.onerror = errorHandler;
                        }
                    }
                    else {
                        for (let i = 0; i < length; ++i) {
                            reqs.push(req = store[type](args1[i]));
                            req.onerror = errorHandler;
                        }
                    }
                }
                const done = event => {
                    const lastResult = event.target.result;
                    for (let i = reqs.length; i--;) {
                        if (reqs[i].error != null)
                            failures[i] = reqs[i].error;
                        reqs[i] = reqs[i].result;
                    }
                    resolve({
                        numFailures,
                        failures,
                        results: type === "delete" ? keys : reqs,
                        lastResult
                    });
                };
                req.onerror = event => {
                    errorHandler(event);
                    done(event);
                };
                req.onsuccess = done;
            });
        }
        function openCursor({ trans, values, query, reverse, unique }) {
            return new Promise((resolve, reject) => {
                resolve = wrap(resolve);
                const { index, range } = query;
                const store = trans.objectStore(tableName);
                const source = index.isPrimaryKey ?
                    store :
                    store.index(index.name);
                const direction = reverse ?
                    unique ?
                        "prevunique" :
                        "prev" :
                    unique ?
                        "nextunique" :
                        "next";
                const req = values || !('openKeyCursor' in source) ?
                    source.openCursor(makeIDBKeyRange(range), direction) :
                    source.openKeyCursor(makeIDBKeyRange(range), direction);
                req.onerror = eventRejectHandler(reject);
                req.onsuccess = wrap(ev => {
                    const cursor = req.result;
                    if (!cursor) {
                        resolve(null);
                        return;
                    }
                    cursor.___id = ++_id_counter;
                    cursor.done = false;
                    const _cursorContinue = cursor.continue.bind(cursor);
                    let _cursorContinuePrimaryKey = cursor.continuePrimaryKey;
                    if (_cursorContinuePrimaryKey)
                        _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor);
                    const _cursorAdvance = cursor.advance.bind(cursor);
                    const doThrowCursorIsNotStarted = () => { throw new Error("Cursor not started"); };
                    const doThrowCursorIsStopped = () => { throw new Error("Cursor not stopped"); };
                    cursor.trans = trans;
                    cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted;
                    cursor.fail = wrap(reject);
                    cursor.next = function () {
                        let gotOne = 1;
                        return this.start(() => gotOne-- ? this.continue() : this.stop()).then(() => this);
                    };
                    cursor.start = (callback) => {
                        const iterationPromise = new Promise((resolveIteration, rejectIteration) => {
                            resolveIteration = wrap(resolveIteration);
                            req.onerror = eventRejectHandler(rejectIteration);
                            cursor.fail = rejectIteration;
                            cursor.stop = value => {
                                cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped;
                                resolveIteration(value);
                            };
                        });
                        const guardedCallback = () => {
                            if (req.result) {
                                try {
                                    callback();
                                }
                                catch (err) {
                                    cursor.fail(err);
                                }
                            }
                            else {
                                cursor.done = true;
                                cursor.start = () => { throw new Error("Cursor behind last entry"); };
                                cursor.stop();
                            }
                        };
                        req.onsuccess = wrap(ev => {
                            req.onsuccess = guardedCallback;
                            guardedCallback();
                        });
                        cursor.continue = _cursorContinue;
                        cursor.continuePrimaryKey = _cursorContinuePrimaryKey;
                        cursor.advance = _cursorAdvance;
                        guardedCallback();
                        return iterationPromise;
                    };
                    resolve(cursor);
                }, reject);
            });
        }
        function query(hasGetAll) {
            return (request) => {
                return new Promise((resolve, reject) => {
                    resolve = wrap(resolve);
                    const { trans, values, limit, query } = request;
                    const nonInfinitLimit = limit === Infinity ? undefined : limit;
                    const { index, range } = query;
                    const store = trans.objectStore(tableName);
                    const source = index.isPrimaryKey ? store : store.index(index.name);
                    const idbKeyRange = makeIDBKeyRange(range);
                    if (limit === 0)
                        return resolve({ result: [] });
                    if (hasGetAll) {
                        const req = values ?
                            source.getAll(idbKeyRange, nonInfinitLimit) :
                            source.getAllKeys(idbKeyRange, nonInfinitLimit);
                        req.onsuccess = event => resolve({ result: event.target.result });
                        req.onerror = eventRejectHandler(reject);
                    }
                    else {
                        let count = 0;
                        const req = values || !('openKeyCursor' in source) ?
                            source.openCursor(idbKeyRange) :
                            source.openKeyCursor(idbKeyRange);
                        const result = [];
                        req.onsuccess = event => {
                            const cursor = req.result;
                            if (!cursor)
                                return resolve({ result });
                            result.push(values ? cursor.value : cursor.primaryKey);
                            if (++count === limit)
                                return resolve({ result });
                            cursor.continue();
                        };
                        req.onerror = eventRejectHandler(reject);
                    }
                });
            };
        }
        return {
            name: tableName,
            schema: tableSchema,
            mutate,
            getMany({ trans, keys, method = 'get' }) {
                return new Promise((resolve, reject) => {
                    resolve = wrap(resolve);
                    const store = trans.objectStore(tableName);
                    const length = keys.length;
                    const result = new Array(length);
                    let keyCount = 0;
                    let callbackCount = 0;
                    let req;
                    const successHandler = event => {
                        const req = event.target;
                        if ((result[req._pos] = req.result) != null)
                            ;
                        if (++callbackCount === keyCount)
                            resolve(result);
                    };
                    const errorHandler = eventRejectHandler(reject);
                    for (let i = 0; i < length; ++i) {
                        const key = keys[i];
                        if (key != null) {
                            req = store[method](keys[i]);
                            req._pos = i;
                            req.onsuccess = successHandler;
                            req.onerror = errorHandler;
                            ++keyCount;
                        }
                    }
                    if (keyCount === 0)
                        resolve(result);
                });
            },
            get({ trans, key, method = 'get' }) {
                return new Promise((resolve, reject) => {
                    resolve = wrap(resolve);
                    const store = trans.objectStore(tableName);
                    const req = store[method](key);
                    req.onsuccess = event => resolve(event.target.result);
                    req.onerror = eventRejectHandler(reject);
                });
            },
            query: query(hasGetAll),
            openCursor,
            count({ query, trans }) {
                const { index, range } = query;
                return new Promise((resolve, reject) => {
                    const store = trans.objectStore(tableName);
                    const source = index.isPrimaryKey ? store : store.index(index.name);
                    const idbKeyRange = makeIDBKeyRange(range);
                    const req = idbKeyRange ? source.count(idbKeyRange) : source.count();
                    req.onsuccess = wrap(ev => resolve(ev.target.result));
                    req.onerror = eventRejectHandler(reject);
                });
            }
        };
    }
    const { schema, hasGetAll } = extractSchema(db, tmpTrans);
    const tables = schema.tables.map(tableSchema => createDbCoreTable(tableSchema));
    const tableMap = {};
    tables.forEach(table => tableMap[table.name] = table);
    return {
        stack: "dbcore",
        transaction: db.transaction.bind(db),
        table(name) {
            const result = tableMap[name];
            if (!result)
                throw new Error(`Table '${name}' not found`);
            return tableMap[name];
        },
        MIN_KEY: -Infinity,
        MAX_KEY: getMaxKey(IdbKeyRange),
        schema
    };
}

function createMiddlewareStack(stackImpl, middlewares) {
    return middlewares.reduce((down, { create }) => ({ ...down, ...create(down) }), stackImpl);
}
function createMiddlewareStacks(middlewares, idbdb, { IDBKeyRange, indexedDB }, tmpTrans) {
    const dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore);
    return {
        dbcore
    };
}
function generateMiddlewareStacks({ _novip: db }, tmpTrans) {
    const idbdb = tmpTrans.db;
    const stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans);
    db.core = stacks.dbcore;
    db.tables.forEach(table => {
        const tableName = table.name;
        if (db.core.schema.tables.some(tbl => tbl.name === tableName)) {
            table.core = db.core.table(tableName);
            if (db[tableName] instanceof db.Table) {
                db[tableName].core = table.core;
            }
        }
    });
}

function setApiOnPlace({ _novip: db }, objs, tableNames, dbschema) {
    tableNames.forEach(tableName => {
        const schema = dbschema[tableName];
        objs.forEach(obj => {
            const propDesc = getPropertyDescriptor(obj, tableName);
            if (!propDesc || ("value" in propDesc && propDesc.value === undefined)) {
                if (obj === db.Transaction.prototype || obj instanceof db.Transaction) {
                    setProp(obj, tableName, {
                        get() { return this.table(tableName); },
                        set(value) {
                            defineProperty(this, tableName, { value, writable: true, configurable: true, enumerable: true });
                        }
                    });
                }
                else {
                    obj[tableName] = new db.Table(tableName, schema);
                }
            }
        });
    });
}
function removeTablesApi({ _novip: db }, objs) {
    objs.forEach(obj => {
        for (let key in obj) {
            if (obj[key] instanceof db.Table)
                delete obj[key];
        }
    });
}
function lowerVersionFirst(a, b) {
    return a._cfg.version - b._cfg.version;
}
function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) {
    const globalSchema = db._dbSchema;
    const trans = db._createTransaction('readwrite', db._storeNames, globalSchema);
    trans.create(idbUpgradeTrans);
    trans._completion.catch(reject);
    const rejectTransaction = trans._reject.bind(trans);
    const transless = PSD.transless || PSD;
    newScope(() => {
        PSD.trans = trans;
        PSD.transless = transless;
        if (oldVersion === 0) {
            keys(globalSchema).forEach(tableName => {
                createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes);
            });
            generateMiddlewareStacks(db, idbUpgradeTrans);
            DexiePromise.follow(() => db.on.populate.fire(trans)).catch(rejectTransaction);
        }
        else
            updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans).catch(rejectTransaction);
    });
}
function updateTablesAndIndexes({ _novip: db }, oldVersion, trans, idbUpgradeTrans) {
    const queue = [];
    const versions = db._versions;
    let globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans);
    let anyContentUpgraderHasRun = false;
    const versToRun = versions.filter(v => v._cfg.version >= oldVersion);
    versToRun.forEach(version => {
        queue.push(() => {
            const oldSchema = globalSchema;
            const newSchema = version._cfg.dbschema;
            adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans);
            adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans);
            globalSchema = db._dbSchema = newSchema;
            const diff = getSchemaDiff(oldSchema, newSchema);
            diff.add.forEach(tuple => {
                createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes);
            });
            diff.change.forEach(change => {
                if (change.recreate) {
                    throw new exceptions.Upgrade("Not yet support for changing primary key");
                }
                else {
                    const store = idbUpgradeTrans.objectStore(change.name);
                    change.add.forEach(idx => addIndex(store, idx));
                    change.change.forEach(idx => {
                        store.deleteIndex(idx.name);
                        addIndex(store, idx);
                    });
                    change.del.forEach(idxName => store.deleteIndex(idxName));
                }
            });
            const contentUpgrade = version._cfg.contentUpgrade;
            if (contentUpgrade && version._cfg.version > oldVersion) {
                generateMiddlewareStacks(db, idbUpgradeTrans);
                trans._memoizedTables = {};
                anyContentUpgraderHasRun = true;
                let upgradeSchema = shallowClone(newSchema);
                diff.del.forEach(table => {
                    upgradeSchema[table] = oldSchema[table];
                });
                removeTablesApi(db, [db.Transaction.prototype]);
                setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema), upgradeSchema);
                trans.schema = upgradeSchema;
                const contentUpgradeIsAsync = isAsyncFunction(contentUpgrade);
                if (contentUpgradeIsAsync) {
                    incrementExpectedAwaits();
                }
                let returnValue;
                const promiseFollowed = DexiePromise.follow(() => {
                    returnValue = contentUpgrade(trans);
                    if (returnValue) {
                        if (contentUpgradeIsAsync) {
                            var decrementor = decrementExpectedAwaits.bind(null, null);
                            returnValue.then(decrementor, decrementor);
                        }
                    }
                });
                return (returnValue && typeof returnValue.then === 'function' ?
                    DexiePromise.resolve(returnValue) : promiseFollowed.then(() => returnValue));
            }
        });
        queue.push(idbtrans => {
            if (!anyContentUpgraderHasRun || !hasIEDeleteObjectStoreBug) {
                const newSchema = version._cfg.dbschema;
                deleteRemovedTables(newSchema, idbtrans);
            }
            removeTablesApi(db, [db.Transaction.prototype]);
            setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema);
            trans.schema = db._dbSchema;
        });
    });
    function runQueue() {
        return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) :
            DexiePromise.resolve();
    }
    return runQueue().then(() => {
        createMissingTables(globalSchema, idbUpgradeTrans);
    });
}
function getSchemaDiff(oldSchema, newSchema) {
    const diff = {
        del: [],
        add: [],
        change: []
    };
    let table;
    for (table in oldSchema) {
        if (!newSchema[table])
            diff.del.push(table);
    }
    for (table in newSchema) {
        const oldDef = oldSchema[table], newDef = newSchema[table];
        if (!oldDef) {
            diff.add.push([table, newDef]);
        }
        else {
            const change = {
                name: table,
                def: newDef,
                recreate: false,
                del: [],
                add: [],
                change: []
            };
            if ((
            '' + (oldDef.primKey.keyPath || '')) !== ('' + (newDef.primKey.keyPath || '')) ||
                (oldDef.primKey.auto !== newDef.primKey.auto && !isIEOrEdge))
             {
                change.recreate = true;
                diff.change.push(change);
            }
            else {
                const oldIndexes = oldDef.idxByName;
                const newIndexes = newDef.idxByName;
                let idxName;
                for (idxName in oldIndexes) {
                    if (!newIndexes[idxName])
                        change.del.push(idxName);
                }
                for (idxName in newIndexes) {
                    const oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName];
                    if (!oldIdx)
                        change.add.push(newIdx);
                    else if (oldIdx.src !== newIdx.src)
                        change.change.push(newIdx);
                }
                if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) {
                    diff.change.push(change);
                }
            }
        }
    }
    return diff;
}
function createTable(idbtrans, tableName, primKey, indexes) {
    const store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ?
        { keyPath: primKey.keyPath, autoIncrement: primKey.auto } :
        { autoIncrement: primKey.auto });
    indexes.forEach(idx => addIndex(store, idx));
    return store;
}
function createMissingTables(newSchema, idbtrans) {
    keys(newSchema).forEach(tableName => {
        if (!idbtrans.db.objectStoreNames.contains(tableName)) {
            createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes);
        }
    });
}
function deleteRemovedTables(newSchema, idbtrans) {
    [].slice.call(idbtrans.db.objectStoreNames).forEach(storeName => newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName));
}
function addIndex(store, idx) {
    store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi });
}
function buildGlobalSchema(db, idbdb, tmpTrans) {
    const globalSchema = {};
    const dbStoreNames = slice(idbdb.objectStoreNames, 0);
    dbStoreNames.forEach(storeName => {
        const store = tmpTrans.objectStore(storeName);
        let keyPath = store.keyPath;
        const primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", false, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true);
        const indexes = [];
        for (let j = 0; j < store.indexNames.length; ++j) {
            const idbindex = store.index(store.indexNames[j]);
            keyPath = idbindex.keyPath;
            var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false);
            indexes.push(index);
        }
        globalSchema[storeName] = createTableSchema(storeName, primKey, indexes);
    });
    return globalSchema;
}
function readGlobalSchema({ _novip: db }, idbdb, tmpTrans) {
    db.verno = idbdb.version / 10;
    const globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans);
    db._storeNames = slice(idbdb.objectStoreNames, 0);
    setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema);
}
function verifyInstalledSchema(db, tmpTrans) {
    const installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans);
    const diff = getSchemaDiff(installedSchema, db._dbSchema);
    return !(diff.add.length || diff.change.some(ch => ch.add.length || ch.change.length));
}
function adjustToExistingIndexNames({ _novip: db }, schema, idbtrans) {
    const storeNames = idbtrans.db.objectStoreNames;
    for (let i = 0; i < storeNames.length; ++i) {
        const storeName = storeNames[i];
        const store = idbtrans.objectStore(storeName);
        db._hasGetAll = 'getAll' in store;
        for (let j = 0; j < store.indexNames.length; ++j) {
            const indexName = store.indexNames[j];
            const keyPath = store.index(indexName).keyPath;
            const dexieName = typeof keyPath === 'string' ? keyPath : "[" + slice(keyPath).join('+') + "]";
            if (schema[storeName]) {
                const indexSpec = schema[storeName].idxByName[dexieName];
                if (indexSpec) {
                    indexSpec.name = indexName;
                    delete schema[storeName].idxByName[dexieName];
                    schema[storeName].idxByName[indexName] = indexSpec;
                }
            }
        }
    }
    if (typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) &&
        !/(Chrome\/|Edge\/)/.test(navigator.userAgent) &&
        _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope &&
        [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) {
        db._hasGetAll = false;
    }
}
function parseIndexSyntax(primKeyAndIndexes) {
    return primKeyAndIndexes.split(',').map((index, indexNum) => {
        index = index.trim();
        const name = index.replace(/([&*]|\+\+)/g, "");
        const keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name;
        return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray(keyPath), indexNum === 0);
    });
}

class Version {
    _parseStoresSpec(stores, outSchema) {
        keys(stores).forEach(tableName => {
            if (stores[tableName] !== null) {
                var indexes = parseIndexSyntax(stores[tableName]);
                var primKey = indexes.shift();
                if (primKey.multi)
                    throw new exceptions.Schema("Primary key cannot be multi-valued");
                indexes.forEach(idx => {
                    if (idx.auto)
                        throw new exceptions.Schema("Only primary key can be marked as autoIncrement (++)");
                    if (!idx.keyPath)
                        throw new exceptions.Schema("Index must have a name and cannot be an empty string");
                });
                outSchema[tableName] = createTableSchema(tableName, primKey, indexes);
            }
        });
    }
    stores(stores) {
        const db = this.db;
        this._cfg.storesSource = this._cfg.storesSource ?
            extend(this._cfg.storesSource, stores) :
            stores;
        const versions = db._versions;
        const storesSpec = {};
        let dbschema = {};
        versions.forEach(version => {
            extend(storesSpec, version._cfg.storesSource);
            dbschema = (version._cfg.dbschema = {});
            version._parseStoresSpec(storesSpec, dbschema);
        });
        db._dbSchema = dbschema;
        removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]);
        setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema);
        db._storeNames = keys(dbschema);
        return this;
    }
    upgrade(upgradeFunction) {
        this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction);
        return this;
    }
}

function createVersionConstructor(db) {
    return makeClassConstructor(Version.prototype, function Version(versionNumber) {
        this.db = db;
        this._cfg = {
            version: versionNumber,
            storesSource: null,
            dbschema: {},
            tables: {},
            contentUpgrade: null
        };
    });
}

function getDbNamesTable(indexedDB, IDBKeyRange) {
    let dbNamesDB = indexedDB["_dbNamesDB"];
    if (!dbNamesDB) {
        dbNamesDB = indexedDB["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, {
            addons: [],
            indexedDB,
            IDBKeyRange,
        });
        dbNamesDB.version(1).stores({ dbnames: "name" });
    }
    return dbNamesDB.table("dbnames");
}
function hasDatabasesNative(indexedDB) {
    return indexedDB && typeof indexedDB.databases === "function";
}
function getDatabaseNames({ indexedDB, IDBKeyRange, }) {
    return hasDatabasesNative(indexedDB)
        ? Promise.resolve(indexedDB.databases()).then((infos) => infos
            .map((info) => info.name)
            .filter((name) => name !== DBNAMES_DB))
        : getDbNamesTable(indexedDB, IDBKeyRange).toCollection().primaryKeys();
}
function _onDatabaseCreated({ indexedDB, IDBKeyRange }, name) {
    !hasDatabasesNative(indexedDB) &&
        name !== DBNAMES_DB &&
        getDbNamesTable(indexedDB, IDBKeyRange).put({ name }).catch(nop);
}
function _onDatabaseDeleted({ indexedDB, IDBKeyRange }, name) {
    !hasDatabasesNative(indexedDB) &&
        name !== DBNAMES_DB &&
        getDbNamesTable(indexedDB, IDBKeyRange).delete(name).catch(nop);
}

function vip(fn) {
    return newScope(function () {
        PSD.letThrough = true;
        return fn();
    });
}

function idbReady() {
    var isSafari = !navigator.userAgentData &&
        /Safari\//.test(navigator.userAgent) &&
        !/Chrom(e|ium)\//.test(navigator.userAgent);
    if (!isSafari || !indexedDB.databases)
        return Promise.resolve();
    var intervalId;
    return new Promise(function (resolve) {
        var tryIdb = function () { return indexedDB.databases().finally(resolve); };
        intervalId = setInterval(tryIdb, 100);
        tryIdb();
    }).finally(function () { return clearInterval(intervalId); });
}

function dexieOpen(db) {
    const state = db._state;
    const { indexedDB } = db._deps;
    if (state.isBeingOpened || db.idbdb)
        return state.dbReadyPromise.then(() => state.dbOpenError ?
            rejection(state.dbOpenError) :
            db);
    debug && (state.openCanceller._stackHolder = getErrorWithStack());
    state.isBeingOpened = true;
    state.dbOpenError = null;
    state.openComplete = false;
    const openCanceller = state.openCanceller;
    function throwIfCancelled() {
        if (state.openCanceller !== openCanceller)
            throw new exceptions.DatabaseClosed('db.open() was cancelled');
    }
    let resolveDbReady = state.dbReadyResolve,
    upgradeTransaction = null, wasCreated = false;
    return DexiePromise.race([openCanceller, (typeof navigator === 'undefined' ? DexiePromise.resolve() : idbReady()).then(() => new DexiePromise((resolve, reject) => {
            throwIfCancelled();
            if (!indexedDB)
                throw new exceptions.MissingAPI();
            const dbName = db.name;
            const req = state.autoSchema ?
                indexedDB.open(dbName) :
                indexedDB.open(dbName, Math.round(db.verno * 10));
            if (!req)
                throw new exceptions.MissingAPI();
            req.onerror = eventRejectHandler(reject);
            req.onblocked = wrap(db._fireOnBlocked);
            req.onupgradeneeded = wrap(e => {
                upgradeTransaction = req.transaction;
                if (state.autoSchema && !db._options.allowEmptyDB) {
                    req.onerror = preventDefault;
                    upgradeTransaction.abort();
                    req.result.close();
                    const delreq = indexedDB.deleteDatabase(dbName);
                    delreq.onsuccess = delreq.onerror = wrap(() => {
                        reject(new exceptions.NoSuchDatabase(`Database ${dbName} doesnt exist`));
                    });
                }
                else {
                    upgradeTransaction.onerror = eventRejectHandler(reject);
                    var oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion;
                    wasCreated = oldVer < 1;
                    db._novip.idbdb = req.result;
                    runUpgraders(db, oldVer / 10, upgradeTransaction, reject);
                }
            }, reject);
            req.onsuccess = wrap(() => {
                upgradeTransaction = null;
                const idbdb = db._novip.idbdb = req.result;
                const objectStoreNames = slice(idbdb.objectStoreNames);
                if (objectStoreNames.length > 0)
                    try {
                        const tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), 'readonly');
                        if (state.autoSchema)
                            readGlobalSchema(db, idbdb, tmpTrans);
                        else {
                            adjustToExistingIndexNames(db, db._dbSchema, tmpTrans);
                            if (!verifyInstalledSchema(db, tmpTrans)) {
                                console.warn(`Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Some queries may fail.`);
                            }
                        }
                        generateMiddlewareStacks(db, tmpTrans);
                    }
                    catch (e) {
                    }
                connections.push(db);
                idbdb.onversionchange = wrap(ev => {
                    state.vcFired = true;
                    db.on("versionchange").fire(ev);
                });
                idbdb.onclose = wrap(ev => {
                    db.on("close").fire(ev);
                });
                if (wasCreated)
                    _onDatabaseCreated(db._deps, dbName);
                resolve();
            }, reject);
        }))]).then(() => {
        throwIfCancelled();
        state.onReadyBeingFired = [];
        return DexiePromise.resolve(vip(() => db.on.ready.fire(db.vip))).then(function fireRemainders() {
            if (state.onReadyBeingFired.length > 0) {
                let remainders = state.onReadyBeingFired.reduce(promisableChain, nop);
                state.onReadyBeingFired = [];
                return DexiePromise.resolve(vip(() => remainders(db.vip))).then(fireRemainders);
            }
        });
    }).finally(() => {
        state.onReadyBeingFired = null;
        state.isBeingOpened = false;
    }).then(() => {
        return db;
    }).catch(err => {
        state.dbOpenError = err;
        try {
            upgradeTransaction && upgradeTransaction.abort();
        }
        catch (_a) { }
        if (openCanceller === state.openCanceller) {
            db._close();
        }
        return rejection(err);
    }).finally(() => {
        state.openComplete = true;
        resolveDbReady();
    });
}

function awaitIterator(iterator) {
    var callNext = result => iterator.next(result), doThrow = error => iterator.throw(error), onSuccess = step(callNext), onError = step(doThrow);
    function step(getNext) {
        return (val) => {
            var next = getNext(val), value = next.value;
            return next.done ? value :
                (!value || typeof value.then !== 'function' ?
                    isArray(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) :
                    value.then(onSuccess, onError));
        };
    }
    return step(callNext)();
}

function extractTransactionArgs(mode, _tableArgs_, scopeFunc) {
    var i = arguments.length;
    if (i < 2)
        throw new exceptions.InvalidArgument("Too few arguments");
    var args = new Array(i - 1);
    while (--i)
        args[i - 1] = arguments[i];
    scopeFunc = args.pop();
    var tables = flatten(args);
    return [mode, tables, scopeFunc];
}
function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) {
    return DexiePromise.resolve().then(() => {
        const transless = PSD.transless || PSD;
        const trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction);
        const zoneProps = {
            trans: trans,
            transless: transless
        };
        if (parentTransaction) {
            trans.idbtrans = parentTransaction.idbtrans;
        }
        else {
            trans.create();
        }
        const scopeFuncIsAsync = isAsyncFunction(scopeFunc);
        if (scopeFuncIsAsync) {
            incrementExpectedAwaits();
        }
        let returnValue;
        const promiseFollowed = DexiePromise.follow(() => {
            returnValue = scopeFunc.call(trans, trans);
            if (returnValue) {
                if (scopeFuncIsAsync) {
                    var decrementor = decrementExpectedAwaits.bind(null, null);
                    returnValue.then(decrementor, decrementor);
                }
                else if (typeof returnValue.next === 'function' && typeof returnValue.throw === 'function') {
                    returnValue = awaitIterator(returnValue);
                }
            }
        }, zoneProps);
        return (returnValue && typeof returnValue.then === 'function' ?
            DexiePromise.resolve(returnValue).then(x => trans.active ?
                x
                : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn")))
            : promiseFollowed.then(() => returnValue)).then(x => {
            if (parentTransaction)
                trans._resolve();
            return trans._completion.then(() => x);
        }).catch(e => {
            trans._reject(e);
            return rejection(e);
        });
    });
}

function pad(a, value, count) {
    const result = isArray(a) ? a.slice() : [a];
    for (let i = 0; i < count; ++i)
        result.push(value);
    return result;
}
function createVirtualIndexMiddleware(down) {
    return {
        ...down,
        table(tableName) {
            const table = down.table(tableName);
            const { schema } = table;
            const indexLookup = {};
            const allVirtualIndexes = [];
            function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) {
                const keyPathAlias = getKeyPathAlias(keyPath);
                const indexList = (indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []);
                const keyLength = keyPath == null ? 0 : typeof keyPath === 'string' ? 1 : keyPath.length;
                const isVirtual = keyTail > 0;
                const virtualIndex = {
                    ...lowLevelIndex,
                    isVirtual,
                    keyTail,
                    keyLength,
                    extractKey: getKeyExtractor(keyPath),
                    unique: !isVirtual && lowLevelIndex.unique
                };
                indexList.push(virtualIndex);
                if (!virtualIndex.isPrimaryKey) {
                    allVirtualIndexes.push(virtualIndex);
                }
                if (keyLength > 1) {
                    const virtualKeyPath = keyLength === 2 ?
                        keyPath[0] :
                        keyPath.slice(0, keyLength - 1);
                    addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex);
                }
                indexList.sort((a, b) => a.keyTail - b.keyTail);
                return virtualIndex;
            }
            const primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey);
            indexLookup[":id"] = [primaryKey];
            for (const index of schema.indexes) {
                addVirtualIndexes(index.keyPath, 0, index);
            }
            function findBestIndex(keyPath) {
                const result = indexLookup[getKeyPathAlias(keyPath)];
                return result && result[0];
            }
            function translateRange(range, keyTail) {
                return {
                    type: range.type === 1  ?
                        2  :
                        range.type,
                    lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail),
                    lowerOpen: true,
                    upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail),
                    upperOpen: true
                };
            }
            function translateRequest(req) {
                const index = req.query.index;
                return index.isVirtual ? {
                    ...req,
                    query: {
                        index,
                        range: translateRange(req.query.range, index.keyTail)
                    }
                } : req;
            }
            const result = {
                ...table,
                schema: {
                    ...schema,
                    primaryKey,
                    indexes: allVirtualIndexes,
                    getIndexByKeyPath: findBestIndex
                },
                count(req) {
                    return table.count(translateRequest(req));
                },
                query(req) {
                    return table.query(translateRequest(req));
                },
                openCursor(req) {
                    const { keyTail, isVirtual, keyLength } = req.query.index;
                    if (!isVirtual)
                        return table.openCursor(req);
                    function createVirtualCursor(cursor) {
                        function _continue(key) {
                            key != null ?
                                cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) :
                                req.unique ?
                                    cursor.continue(cursor.key.slice(0, keyLength)
                                        .concat(req.reverse
                                        ? down.MIN_KEY
                                        : down.MAX_KEY, keyTail)) :
                                    cursor.continue();
                        }
                        const virtualCursor = Object.create(cursor, {
                            continue: { value: _continue },
                            continuePrimaryKey: {
                                value(key, primaryKey) {
                                    cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey);
                                }
                            },
                            primaryKey: {
                                get() {
                                    return cursor.primaryKey;
                                }
                            },
                            key: {
                                get() {
                                    const key = cursor.key;
                                    return keyLength === 1 ?
                                        key[0] :
                                        key.slice(0, keyLength);
                                }
                            },
                            value: {
                                get() {
                                    return cursor.value;
                                }
                            }
                        });
                        return virtualCursor;
                    }
                    return table.openCursor(translateRequest(req))
                        .then(cursor => cursor && createVirtualCursor(cursor));
                }
            };
            return result;
        }
    };
}
const virtualIndexMiddleware = {
    stack: "dbcore",
    name: "VirtualIndexMiddleware",
    level: 1,
    create: createVirtualIndexMiddleware
};

class Dexie$1 {
    constructor(name, options) {
        this._middlewares = {};
        this.verno = 0;
        const deps = Dexie$1.dependencies;
        this._options = options = {
            addons: Dexie$1.addons,
            autoOpen: true,
            indexedDB: deps.indexedDB,
            IDBKeyRange: deps.IDBKeyRange,
            ...options
        };
        this._deps = {
            indexedDB: options.indexedDB,
            IDBKeyRange: options.IDBKeyRange
        };
        const { addons, } = options;
        this._dbSchema = {};
        this._versions = [];
        this._storeNames = [];
        this._allTables = {};
        this.idbdb = null;
        this._novip = this;
        const state = {
            dbOpenError: null,
            isBeingOpened: false,
            onReadyBeingFired: null,
            openComplete: false,
            dbReadyResolve: nop,
            dbReadyPromise: null,
            cancelOpen: nop,
            openCanceller: null,
            autoSchema: true
        };
        state.dbReadyPromise = new DexiePromise(resolve => {
            state.dbReadyResolve = resolve;
        });
        state.openCanceller = new DexiePromise((_, reject) => {
            state.cancelOpen = reject;
        });
        this._state = state;
        this.name = name;
        this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] });
        this.on.ready.subscribe = override(this.on.ready.subscribe, subscribe => {
            return (subscriber, bSticky) => {
                Dexie$1.vip(() => {
                    const state = this._state;
                    if (state.openComplete) {
                        if (!state.dbOpenError)
                            DexiePromise.resolve().then(subscriber);
                        if (bSticky)
                            subscribe(subscriber);
                    }
                    else if (state.onReadyBeingFired) {
                        state.onReadyBeingFired.push(subscriber);
                        if (bSticky)
                            subscribe(subscriber);
                    }
                    else {
                        subscribe(subscriber);
                        const db = this;
                        if (!bSticky)
                            subscribe(function unsubscribe() {
                                db.on.ready.unsubscribe(subscriber);
                                db.on.ready.unsubscribe(unsubscribe);
                            });
                    }
                });
            };
        });
        this.Collection = createCollectionConstructor(this);
        this.Table = createTableConstructor(this);
        this.Transaction = createTransactionConstructor(this);
        this.Version = createVersionConstructor(this);
        this.WhereClause = createWhereClauseConstructor(this);
        this.on("versionchange", ev => {
            if (ev.newVersion > 0)
                console.warn(`Another connection wants to upgrade database '${this.name}'. Closing db now to resume the upgrade.`);
            else
                console.warn(`Another connection wants to delete database '${this.name}'. Closing db now to resume the delete request.`);
            this.close();
        });
        this.on("blocked", ev => {
            if (!ev.newVersion || ev.newVersion < ev.oldVersion)
                console.warn(`Dexie.delete('${this.name}') was blocked`);
            else
                console.warn(`Upgrade '${this.name}' blocked by other connection holding version ${ev.oldVersion / 10}`);
        });
        this._maxKey = getMaxKey(options.IDBKeyRange);
        this._createTransaction = (mode, storeNames, dbschema, parentTransaction) => new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction);
        this._fireOnBlocked = ev => {
            this.on("blocked").fire(ev);
            connections
                .filter(c => c.name === this.name && c !== this && !c._state.vcFired)
                .map(c => c.on("versionchange").fire(ev));
        };
        this.use(virtualIndexMiddleware);
        this.vip = Object.create(this, { _vip: { value: true } });
        addons.forEach(addon => addon(this));
    }
    version(versionNumber) {
        if (isNaN(versionNumber) || versionNumber < 0.1)
            throw new exceptions.Type(`Given version is not a positive number`);
        versionNumber = Math.round(versionNumber * 10) / 10;
        if (this.idbdb || this._state.isBeingOpened)
            throw new exceptions.Schema("Cannot add version when database is open");
        this.verno = Math.max(this.verno, versionNumber);
        const versions = this._versions;
        var versionInstance = versions.filter(v => v._cfg.version === versionNumber)[0];
        if (versionInstance)
            return versionInstance;
        versionInstance = new this.Version(versionNumber);
        versions.push(versionInstance);
        versions.sort(lowerVersionFirst);
        versionInstance.stores({});
        this._state.autoSchema = false;
        return versionInstance;
    }
    _whenReady(fn) {
        return (this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip)) ? fn() : new DexiePromise((resolve, reject) => {
            if (this._state.openComplete) {
                return reject(new exceptions.DatabaseClosed(this._state.dbOpenError));
            }
            if (!this._state.isBeingOpened) {
                if (!this._options.autoOpen) {
                    reject(new exceptions.DatabaseClosed());
                    return;
                }
                this.open().catch(nop);
            }
            this._state.dbReadyPromise.then(resolve, reject);
        }).then(fn);
    }
    use({ stack, create, level, name }) {
        if (name)
            this.unuse({ stack, name });
        const middlewares = this._middlewares[stack] || (this._middlewares[stack] = []);
        middlewares.push({ stack, create, level: level == null ? 10 : level, name });
        middlewares.sort((a, b) => a.level - b.level);
        return this;
    }
    unuse({ stack, name, create }) {
        if (stack && this._middlewares[stack]) {
            this._middlewares[stack] = this._middlewares[stack].filter(mw => create ? mw.create !== create :
                name ? mw.name !== name :
                    false);
        }
        return this;
    }
    open() {
        return dexieOpen(this);
    }
    _close() {
        const state = this._state;
        const idx = connections.indexOf(this);
        if (idx >= 0)
            connections.splice(idx, 1);
        if (this.idbdb) {
            try {
                this.idbdb.close();
            }
            catch (e) { }
            this._novip.idbdb = null;
        }
        state.dbReadyPromise = new DexiePromise(resolve => {
            state.dbReadyResolve = resolve;
        });
        state.openCanceller = new DexiePromise((_, reject) => {
            state.cancelOpen = reject;
        });
    }
    close() {
        this._close();
        const state = this._state;
        this._options.autoOpen = false;
        state.dbOpenError = new exceptions.DatabaseClosed();
        if (state.isBeingOpened)
            state.cancelOpen(state.dbOpenError);
    }
    delete() {
        const hasArguments = arguments.length > 0;
        const state = this._state;
        return new DexiePromise((resolve, reject) => {
            const doDelete = () => {
                this.close();
                var req = this._deps.indexedDB.deleteDatabase(this.name);
                req.onsuccess = wrap(() => {
                    _onDatabaseDeleted(this._deps, this.name);
                    resolve();
                });
                req.onerror = eventRejectHandler(reject);
                req.onblocked = this._fireOnBlocked;
            };
            if (hasArguments)
                throw new exceptions.InvalidArgument("Arguments not allowed in db.delete()");
            if (state.isBeingOpened) {
                state.dbReadyPromise.then(doDelete);
            }
            else {
                doDelete();
            }
        });
    }
    backendDB() {
        return this.idbdb;
    }
    isOpen() {
        return this.idbdb !== null;
    }
    hasBeenClosed() {
        const dbOpenError = this._state.dbOpenError;
        return dbOpenError && (dbOpenError.name === 'DatabaseClosed');
    }
    hasFailed() {
        return this._state.dbOpenError !== null;
    }
    dynamicallyOpened() {
        return this._state.autoSchema;
    }
    get tables() {
        return keys(this._allTables).map(name => this._allTables[name]);
    }
    transaction() {
        const args = extractTransactionArgs.apply(this, arguments);
        return this._transaction.apply(this, args);
    }
    _transaction(mode, tables, scopeFunc) {
        let parentTransaction = PSD.trans;
        if (!parentTransaction || parentTransaction.db !== this || mode.indexOf('!') !== -1)
            parentTransaction = null;
        const onlyIfCompatible = mode.indexOf('?') !== -1;
        mode = mode.replace('!', '').replace('?', '');
        let idbMode, storeNames;
        try {
            storeNames = tables.map(table => {
                var storeName = table instanceof this.Table ? table.name : table;
                if (typeof storeName !== 'string')
                    throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed");
                return storeName;
            });
            if (mode == "r" || mode === READONLY)
                idbMode = READONLY;
            else if (mode == "rw" || mode == READWRITE)
                idbMode = READWRITE;
            else
                throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode);
            if (parentTransaction) {
                if (parentTransaction.mode === READONLY && idbMode === READWRITE) {
                    if (onlyIfCompatible) {
                        parentTransaction = null;
                    }
                    else
                        throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY");
                }
                if (parentTransaction) {
                    storeNames.forEach(storeName => {
                        if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) {
                            if (onlyIfCompatible) {
                                parentTransaction = null;
                            }
                            else
                                throw new exceptions.SubTransaction("Table " + storeName +
                                    " not included in parent transaction.");
                        }
                    });
                }
                if (onlyIfCompatible && parentTransaction && !parentTransaction.active) {
                    parentTransaction = null;
                }
            }
        }
        catch (e) {
            return parentTransaction ?
                parentTransaction._promise(null, (_, reject) => { reject(e); }) :
                rejection(e);
        }
        const enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc);
        return (parentTransaction ?
            parentTransaction._promise(idbMode, enterTransaction, "lock") :
            PSD.trans ?
                usePSD(PSD.transless, () => this._whenReady(enterTransaction)) :
                this._whenReady(enterTransaction));
    }
    table(tableName) {
        if (!hasOwn(this._allTables, tableName)) {
            throw new exceptions.InvalidTable(`Table ${tableName} does not exist`);
        }
        return this._allTables[tableName];
    }
}

function getObjectDiff(a, b, rv, prfx) {
    rv = rv || {};
    prfx = prfx || '';
    for (const prop in a) {
        if (!hasOwn(b, prop)) {
            rv[prfx + prop] = undefined;
        }
        else {
            const ap = a[prop], bp = b[prop];
            if (typeof ap === 'object' && typeof bp === 'object' && ap && bp) {
                const apTypeName = toStringTag(ap);
                const bpTypeName = toStringTag(bp);
                if (apTypeName !== bpTypeName) {
                    rv[prfx + prop] = b[prop];
                }
                else if (apTypeName === 'Object') {
                    getObjectDiff(ap, bp, rv, prfx + prop + '.');
                }
                else if (ap !== bp) {
                    rv[prfx + prop] = b[prop];
                }
            }
            else if (ap !== bp)
                rv[prfx + prop] = b[prop];
        }
    }
    for (const prop in b) {
        if (!hasOwn(a, prop)) {
            rv[prfx + prop] = b[prop];
        }
    }
    return rv;
}

let domDeps;
try {
    domDeps = {
        indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB,
        IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange
    };
}
catch (e) {
    domDeps = { indexedDB: null, IDBKeyRange: null };
}

const Dexie = Dexie$1;
props(Dexie, {
    ...fullNameExceptions,
    delete(databaseName) {
        const db = new Dexie(databaseName, { addons: [] });
        return db.delete();
    },
    exists(name) {
        return new Dexie(name, { addons: [] }).open().then(db => {
            db.close();
            return true;
        }).catch('NoSuchDatabaseError', () => false);
    },
    getDatabaseNames(cb) {
        try {
            return getDatabaseNames(Dexie.dependencies).then(cb);
        }
        catch (_a) {
            return rejection(new exceptions.MissingAPI());
        }
    },
    defineClass() {
        function Class(content) {
            extend(this, content);
        }
        return Class;
    },
    ignoreTransaction(scopeFunc) {
        return PSD.trans ?
            usePSD(PSD.transless, scopeFunc) :
            scopeFunc();
    },
    vip,
    async: function (generatorFn) {
        return function () {
            try {
                var rv = awaitIterator(generatorFn.apply(this, arguments));
                if (!rv || typeof rv.then !== 'function')
                    return DexiePromise.resolve(rv);
                return rv;
            }
            catch (e) {
                return rejection(e);
            }
        };
    },
    spawn: function (generatorFn, args, thiz) {
        try {
            var rv = awaitIterator(generatorFn.apply(thiz, args || []));
            if (!rv || typeof rv.then !== 'function')
                return DexiePromise.resolve(rv);
            return rv;
        }
        catch (e) {
            return rejection(e);
        }
    },
    currentTransaction: {
        get: () => PSD.trans || null
    },
    waitFor: function (promiseOrFunction, optionalTimeout) {
        const promise = DexiePromise.resolve(typeof promiseOrFunction === 'function' ?
            Dexie.ignoreTransaction(promiseOrFunction) :
            promiseOrFunction)
            .timeout(optionalTimeout || 60000);
        return PSD.trans ?
            PSD.trans.waitFor(promise) :
            promise;
    },
    Promise: DexiePromise,
    debug: {
        get: () => debug,
        set: value => {
            setDebug(value, value === 'dexie' ? () => true : dexieStackFrameFilter);
        }
    },
    derive: derive,
    extend: extend,
    props: props,
    override: override,
    Events: Events,
    getByKeyPath: getByKeyPath,
    setByKeyPath: setByKeyPath,
    delByKeyPath: delByKeyPath,
    shallowClone: shallowClone,
    deepClone: deepClone,
    getObjectDiff: getObjectDiff,
    cmp,
    asap: asap$1,
    minKey: minKey,
    addons: [],
    connections: connections,
    errnames: errnames,
    dependencies: domDeps,
    semVer: DEXIE_VERSION,
    version: DEXIE_VERSION.split('.')
        .map(n => parseInt(n) | 0)
        .reduce((p, c, i) => p + (c / Math.pow(10, i * 2))),
});
Dexie.maxKey = getMaxKey(Dexie.dependencies.IDBKeyRange);

DexiePromise.rejectionMapper = mapError;
setDebug(debug, dexieStackFrameFilter);

var namedExports = /*#__PURE__*/Object.freeze({
__proto__: null,
Dexie: Dexie$1,
'default': Dexie$1
});

Object.assign(Dexie$1, namedExports, { default: Dexie$1 });

return Dexie$1;

}));

/* jshint -W098 */
makeEnum(['MDBOPEN', 'EXECSC', 'LOADINGCLOUD'], 'MEGAFLAG_', window);

// navigate to links internally, not by the browser.
function clickURLs() {
    'use strict';
    var nodeList = document.querySelectorAll('a.clickurl');

    if (nodeList.length) {
        $(nodeList).rebind('click', function() {
            var $this = $(this);
            var url = $this.attr('href') || $this.data('fxhref');
            let eventid = $this.attr('data-eventid') || false;
            const redirect = $this.attr('redirect');
            if (eventid) {
                eventid = parseInt(eventid);
                if (!isNaN(eventid)) {
                    delay(`clickurlevlog${eventid}`, () => eventlog(eventid));
                }
            }

            if (url) {
                var target = $this.attr('target');

                if (target === '_blank') {
                    open(/^(https?:\/\/)/i.test(url) ? url : getBaseUrl() + url, '_blank', 'noopener,noreferrer');
                    return false;
                }

                if (window.loadingDialog && $this.hasClass('pages-nav')) {
                    loadingDialog.quiet = true;
                    onIdle(function() {
                        loadingDialog.quiet = false;
                    });
                }

                if (redirect) {
                    const redirectPage = redirect;
                    login_next = redirectPage === "1" ? `/${page}` : `/${redirectPage}`;
                }

                loadSubPage(url.substr(1));
                return false;
            }
        });
        if (is_extension) {
            $(nodeList).rebind('auxclick', function(e) {

                // if this is middle click on mouse to open it on new tab and this is extension
                if (e.which === 2) {

                    var $this = $(this);
                    var url = $this.attr('href') || $this.data('fxhref');

                    open(getBaseUrl() + url);

                    return false;
                }
            });
        }
    }
    nodeList = undefined;
}

// Handler that deals with scroll to element links.
function scrollToURLs() {
    'use strict';
    var nodeList = document.querySelectorAll('a.scroll_to');

    if (nodeList) {
        $(nodeList).rebind("click", function() {
            var $scrollTo = $($(this).data("scrollto"));

            if ($scrollTo.length) {
                var $toScroll;
                var newOffset = $scrollTo[0].offsetTop;

                if (is_mobile) {
                    var mobileClass = 'body.mobile .fmholder';
                    if (page === "privacy" || page === "terms") {
                        $toScroll = $(mobileClass);
                    }
                }
                else  if ($scrollTo.closest('.ps').length) {
                    $toScroll = $scrollTo.closest('.ps');
                }
                else {
                    $toScroll = $('.fmholder');
                }

                if ($toScroll) {
                    $toScroll.animate({scrollTop: newOffset - 40}, 400);
                }

            }
        });
    }
    nodeList = undefined;
}

/**
 * excludeIntersected
 *
 * Loop through arrays excluding intersected items form array2
 * and prepare result format for tokenInput plugin item format.
 *
 * @param {Array} array1, emails used in share
 * @param {Array} array2, list of all available emails
 *
 * @returns {Array} item An array of JSON objects e.g. { id, name }.
 */
function excludeIntersected(array1, array2) {

    var result = [],
        tmpObj2 = array2;

    if (!array1) {
        return array2;
    }
    else if (!array2) {
        return array1;
    }

    // Loop through emails used in share
    for (var i in array1) {
        if (array1.hasOwnProperty(i)) {

            // Loop through list of all emails
            for (var k in array2) {
                if (array2.hasOwnProperty(k)) {

                    // Remove matched email from result
                    if (array1[i] === array2[k]) {
                        tmpObj2.splice(k, 1);
                        break;
                    }
                }
            }
        }
    }

    // Prepare for token.input plugin item format
    for (var n in tmpObj2) {
        if (tmpObj2.hasOwnProperty(n)) {
            result.push({ id: tmpObj2[n], name: tmpObj2[n] });
        }
    }

    return result;
}

function asciionly(text) {
    var rforeign = /[^\u0000-\u007f]/;
    if (rforeign.test(text)) {
        return false;
    }
    else {
        return true;
    }
}

var isNativeObject = function(obj) {
    var objConstructorText = obj.constructor.toString();
    return objConstructorText.indexOf("[native code]") !== -1 && objConstructorText.indexOf("Object()") === -1;
};

function clone(obj) {

    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (obj instanceof Date) {
        return new Date(obj.getTime());
    }
    if (Array.isArray(obj)) {
        var arr = new Array(obj.length);
        for (var i = obj.length; i--; ) {
            arr[i] = clone(obj[i]);
        }
        return arr;
    }
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) {
                if (!(obj[attr] instanceof Object)) {
                    copy[attr] = obj[attr];
                }
                else if (Array.isArray(obj[attr])) {
                    copy[attr] = clone(obj[attr]);
                }
                else if (!isNativeObject(obj[attr])) {
                    copy[attr] = clone(obj[attr]);
                }
                else if ($.isFunction(obj[attr])) {
                    copy[attr] = obj[attr];
                }
                else {
                    copy[attr] = {};
                }
            }
        }

        return copy;
    }
    else {
        var copy = Object.create(null);

        for (var k in obj) {
            copy[k] = clone(obj[k]);
        }

        return copy;
    }
}

/**
 * Check if something (val) is a string.
 *
 * @param val
 * @returns {boolean}
 */
function isString(val) {
    return (typeof val === 'string' || val instanceof String);
};

function easeOutCubic(t, b, c, d) {
    return c * ((t = t / d - 1) * t * t + 1) + b;
}

function ellipsis(text, location, maxCharacters) {
    "use strict";
    if (!text) {
        return "";
    }

    if (text.length > 0 && text.length > maxCharacters) {
        if (typeof location === 'undefined') {
            location = 'end';
        }
        switch (location) {
            case 'center':
                var center = (maxCharacters / 2);
                text = text.slice(0, center) + '...' + text.slice(-center);
                break;
            case 'end':
                text = text.slice(0, maxCharacters - 3) + '...';
                break;
        }
    }
    return text;
}

function megatitle(nperc) {
    if (!nperc) {
        nperc = '';
    }
    var a = parseInt($('.notification-num:first').text());
    if (a > 0) {
        a = '(' + a + ') ';
    }
    else {
        a = '';
    }
    if (document.title !== a + mega_title + nperc) {
        document.title = a + mega_title + nperc;
    }
}

function countrydetails(isocode) {
    var cdetails = {
        name: M.getCountryName(isocode),
        icon: isocode.toLowerCase() + '.png'
    };
    return cdetails;
}

/**
 * Convert bytes sizes into a human-friendly format (KB, MB, GB), pretty
 * similar to `bytesToSize` but this function returns an object
 * (`{ size: "23,33", unit: 'KB' }`) which is easier to consume
 *
 * @param {Number} bytes Size in bytes to convert
 * @param {Number} [precision] Precision to show the decimal number
 * @param {Boolean} [isSpd] True if this is a speed, unit will be returned as speed unit like KB/s
 * @returns {Object} Returns an object similar to `{size: "2.1", unit: "MB"}`
 */
function numOfBytes(bytes, precision, isSpd) {

    'use strict';

    // If not defined, default to 2dp (this still allows setting precision to 0 for 0dp)
    if (typeof precision === 'undefined') {
        precision = 2;
    }

    var fn = isSpd ? bytesToSpeed : bytesToSize;
    var parts = fn(bytes, precision).split(' ');

    return { size: parts[0], unit: parts[1] || 'B' };
}

function bytesToSize(bytes, precision, format) {
    'use strict'; /* jshint -W074 */

    var s_b = l[20158];
    var s_kb = l[7049];
    var s_mb = l[20159];
    var s_gb = l[17696];
    var s_tb = l[20160];
    var s_pb = l[23061];

    var kilobyte = 1024;
    var megabyte = kilobyte * 1024;
    var gigabyte = megabyte * 1024;
    var terabyte = gigabyte * 1024;
    var petabyte = terabyte * 1024;
    var resultSize = 0;
    var resultUnit = '';
    var capToMB = false;

    if (precision === undefined) {
        if (bytes > gigabyte) {
            precision = 2;
        }
        else if (bytes > megabyte) {
            precision = 1;
        }
    }

    if (format < 0) {
        format = 0;
        capToMB = true;
    }

    if (!bytes) {
        resultSize = 0;
        resultUnit = s_b;
    }
    else if ((bytes >= 0) && (bytes < kilobyte)) {
        resultSize = parseInt(bytes);
        resultUnit = s_b;
    }
    else if ((bytes >= kilobyte) && (bytes < megabyte)) {
        resultSize = (bytes / kilobyte).toFixed(precision);
        resultUnit = s_kb;
    }
    else if ((bytes >= megabyte) && (bytes < gigabyte) || capToMB) {
        resultSize = (bytes / megabyte).toFixed(precision);
        resultUnit = s_mb;
    }
    else if ((bytes >= gigabyte) && (bytes < terabyte)) {
        resultSize = (bytes / gigabyte).toFixed(precision);
        resultUnit = s_gb;
    }
    else if ((bytes >= terabyte) && (bytes < petabyte)) {
        resultSize = (bytes / terabyte).toFixed(precision);
        resultUnit = s_tb;
    }
    else if (bytes >= petabyte) {
        resultSize = (bytes / petabyte).toFixed(precision);
        resultUnit = s_pb;
    }
    else {
        resultSize = parseInt(bytes);
        resultUnit = s_b;
    }

    // Format 4 will return bytes to precision, with trailing 0s
    if (format === 4) {
        resultSize = parseFloat(resultSize);
    }

    if (window.lang !== 'en') {
        // @todo measure the performance degradation by invoking this here now..
        resultSize = mega.intl.decimal.format(resultSize);
    }

    // XXX: If ever adding more HTML here, make sure it's safe and/or sanitize it.
    if (format === 2) {
        return resultSize + '<span>' + resultUnit + '</span>';
    }
    else if (format === 3) {
        return resultSize;
    }
    else if (format && format !== 4) {
        return '<span>' + resultSize + '</span>' + resultUnit;
    }
    else {
        return resultSize + ' ' + resultUnit;
    }
}

/*
 * Very Similar function as bytesToSize due to it is just simple extended version of it by making it as speed.
 * @returns {String} Returns a string that build with value entered and speed unit e.g. 100 KB/s
 */
var bytesToSpeed = function bytesToSpeed() {
    'use strict';
    return l[23062].replace('[%s]', bytesToSize.apply(this, arguments));
};
mBroadcaster.once('startMega', function() {
    'use strict';

    if (lang === 'en' || lang === 'es') {
        bytesToSpeed = function(bytes, precision, format) {
            return bytesToSize(bytes, precision, format) + '/s';
        };
    }
});


function makeid(len) {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    for (var i = 0; i < len; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    return text;
}

/**
 * Checks if the email address is valid using the inbuilt HTML5
 * validation method suggested at https://stackoverflow.com/a/13975255
 * @param {String} email The email address to validate
 * @returns {Boolean} Returns true if email is valid, false if email is invalid
 */
function isValidEmail(email) {

    'use strict';
    // reference to html spec https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type=email)
    // with one modification, that the standard allows emails like khaled@mega
    // which is possible in local environment/networks but not in WWW.
    // so I applied + instead of * at the end
    var regex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
    return regex.test(email);
}

/**
 * Adds on, bind, unbind, one and trigger methods to a specific class's prototype.
 *
 * @param kls class on which prototype this method should add the on, bind, unbind, etc methods
 * @deprecated
 */
function makeObservable(kls) {
    'use strict';
    if (d > 1) {
        console.warn('makeObservable() is deprecated.');
    }
    inherits(kls, MegaDataEmitter);
}

/**
 * Adds simple .setMeta and .getMeta functions, which can be used to store some meta information on the fly.
 * Also triggers `onMetaChange` events (only if the `kls` have a `trigger` method !)
 *
 * @param kls {Class} on which prototype's this method should add the setMeta and getMeta
 */
function makeMetaAware(kls) {
    /**
     * Store meta data
     *
     * @param prefix string
     * @param namespace string
     * @param k string
     * @param val {*}
     */
    kls.prototype.setMeta = function(prefix, namespace, k, val) {
        var self = this;

        if (self["_" + prefix] === undefined) {
            self["_" + prefix] = {};
        }
        if (self["_" + prefix][namespace] === undefined) {
            self["_" + prefix][namespace] = {};
        }
        self["_" + prefix][namespace][k] = val;

        if (self.trigger) {
            self.trigger("onMetaChange", prefix, namespace, k, val);
        }
    };

    /**
     * Clear/delete meta data
     *
     * @param prefix string  optional
     * @param [namespace] string  optional
     * @param [k] string optional
     */
    kls.prototype.clearMeta = function(prefix, namespace, k) {
        var self = this;

        if (!self["_" + prefix]) {
            return;
        }

        if (prefix && !namespace && !k) {
            delete self["_" + prefix];
        }
        else if (prefix && namespace && !k) {
            delete self["_" + prefix][namespace];
        }
        else if (prefix && namespace && k) {
            delete self["_" + prefix][namespace][k];
        }

        if (self.trigger) {
            self.trigger("onMetaChange", prefix, namespace, k);
        }
    };

    /**
     * Retrieve meta data
     *
     * @param prefix {string}
     * @param namespace {string} optional
     * @param k {string} optional
     * @param default_value {*} optional
     * @returns {*}
     */
    kls.prototype.getMeta = function(prefix, namespace, k, default_value) {
        var self = this;

        namespace = namespace || undefined; /* optional */
        k = k || undefined; /* optional */
        default_value = default_value || undefined; /* optional */

        // support for calling only with 2 args.
        if (k === undefined) {
            if (self["_" + prefix] === undefined) {
                return default_value;
            }
            else {
                return self["_" + prefix][namespace] || default_value;
            }
        }
        else {
            // all args

            if (self["_" + prefix] === undefined) {
                return default_value;
            }
            else if (self["_" + prefix][namespace] === undefined) {
                return default_value;
            }
            else {
                return self["_" + prefix][namespace][k] || default_value;
            }
        }
    };
}

/**
 * Gets UAO parameter from the URL if exists and store it
 * @param {String} url          URL
 * @param {String} page         Page
 */
function getUAOParameter(url, page) {
    'use strict';
    var pageLen = page.length;
    if (url.length > pageLen) {
        var urlParams = url.substr(pageLen);
        if (urlParams.length > 14) {
            var uaoParam = urlParams.indexOf('/uao=');
            if (uaoParam > -1) {
                mega.uaoref = urlParams.substr(uaoParam + 5);
            }
        }
    }
}

/**
 * Simple method for generating unique event name with a .suffix that is a hash of the passed 3-n arguments
 * Main purpose is to be used with jQuery.bind and jQuery.unbind.
 *
 * @param eventName {string} event name
 * @param name {string} name of the handler (e.g. .suffix)
 * @returns {string} e.g. $eventName.$name_$ShortHashOfTheAdditionalArguments
 */
function generateEventSuffixFromArguments(eventName, name) {
    var args = Array.prototype.splice.call(arguments, 2);
    var result = "";
    $.each(args, function(k, v) {
        result += v;
    });

    return eventName + "." + name + "_" + ("" + fastHashFunction(result)).replace("-", "_");
}

/**
 * This is a placeholder, which will be used anywhere in our code where we need a simple and FAST hash function.
 * later on, we can change the implementation (to use md5 or murmur) by just changing the function body of this
 * function.
 * @param {String}
 */
function fastHashFunction(val) {
    return MurmurHash3(val, 0x4ef5391a).toString();
}

/**
 * Creates a promise, which will fail if the validateFunction() don't return true in a timely manner (e.g. < timeout).
 *
 * @param validateFunction {Function}
 * @param tick {int}
 * @param timeout {int}
 * @param [waitForPromise] {(MegaPromise|$.Deferred)} Before starting the timer, we will wait for this promise to be rej/res first.
 * @param [name] {String} optional name for the debug output of the error/debug messages
 * @returns {Promise}
 */
function createTimeoutPromise(validateFunction, tick, timeout, waitForPromise, name) {
    'use strict';
    let _res, _rej;
    let running = true;
    let state = 'pending';
    const debug = window.d > 2;
    const tag = (m) => `[${name}] ${m}`;
    const log = (m, ...args) => console.warn(tag(m), ...args);

    const promise = new Promise((resolve, reject) => {
        _rej = reject;
        _res = resolve;

        tick |= 0;
        timeout = Math.max(0, timeout | 0);
        name = `cTP.${name || makeUUID().slice(-17)}.${++mIncID}`;

        if (debug) {
            log('Creating timeout promise...', tick, timeout);
        }
        let threshold = performance.now();

        assert(typeof validateFunction === 'function', tag('Function expected'));
        assert(tick > 100, tag(`at least 100ms are expected, ${tick} provided.`));
        assert(timeout > tick && timeout < 6e5, tag(`Invalid timeout value (${timeout})`));
        validateFunction = tryCatch(validateFunction);

        Promise.resolve(waitForPromise)
            .then(async() => {
                let duration = 0;
                const int = tick / 1e3;

                if (debug) {
                    threshold -= performance.now();

                    if (threshold > 10) {
                        log('Begin took %sms%s', threshold, waitForPromise ? '' : ', tab throttled(?!)');
                    }

                    // @todo add threshold to duration if !waitForPromise (?)
                }

                if (validateFunction()) {
                    if (debug) {
                        log('The validator resolved immediately...', waitForPromise);
                    }
                    state = 'placebo';
                    return -1;
                }

                if (debug) {
                    threshold = performance.now();
                }

                while (running) {
                    await sleep(int);

                    if (debug) {
                        const now = performance.now();
                        const diff = now - threshold;

                        if (diff > tick * 1.8) {
                            log('Tab throttled? did sleep for %sms while %sms were expected...', diff, tick);
                        }
                        threshold = now;
                    }

                    duration += tick;
                    running = !validateFunction();

                    if (duration > timeout) {
                        break;
                    }
                }

                if (running) {
                    if (debug) {
                        log(`Timed out after waiting ${duration}ms`, promise);
                    }

                    state = 'expired';
                    throw new Error('Timed out.');
                }

                if (state === 'aborted') {
                    // xxx: backward compatibility, but rather bogus leaving a dangling promise there..
                    resolve = nop;
                    Object.defineProperty(promise, 'aborted', {value: Date.now()});
                    Object.freeze(promise);
                    return;
                }

                if (debug) {
                    log(`Resolved timeout promise after waiting ${duration}ms...`, promise);
                }
                state = 'fulfilled';

            })
            .then((a0) => resolve(a0))
            .catch(reject);

    }).finally(() => {
        running = false;

        if (debug) {
            createTimeoutPromise.instances.delete(promise);
        }
    });

    if (debug) {
        promise.tick = tick;
        promise.timeout = timeout;
        createTimeoutPromise.instances.add(promise);
    }

    return Object.defineProperties(promise, {
        state: {
            get() {
                return state;
            }
        },

        verify: {
            value: queueMicrotask.bind(null, () => {
                if (validateFunction()) {
                    if (debug) {
                        log("Resolving timeout promise", state, promise);
                    }
                    promise.resolve(0);
                }
            })
        },

        stopTimers: {
            value: () => {
                running = false;
                state = 'aborted';
            }
        },

        name: {
            value: name
        },

        resolve: {
            value: _res
        },

        reject: {
            value: _rej
        }
    });
}

/** @property createTimeoutPromise.instances */
lazy(createTimeoutPromise, 'instances', () => {
    'use strict';
    return new WeakSet();
});

/**
 * Assertion exception.
 * @param message
 *     Message for exception on failure.
 * @constructor
 */
function AssertionFailed(message) {
    this.message = message;
    // karma env?
    this.stack = M && M.getStack ? M.getStack() : String(new Error().stack);
}

AssertionFailed.prototype = Object.create(Error.prototype);
AssertionFailed.prototype.name = 'AssertionFailed';

/**
 * Assert a given test condition.
 *
 * Throws an AssertionFailed exception with a given message, in case the condition is false.
 * The message is assembled by the args following 'test', similar to console.log()
 *
 * @param test
 *     Test statement.
 */
function assert(test) {
    if (test) {
        return;
    }
    //assemble message from parameters
    var message = '';
    var last = arguments.length - 1;
    for (var i = 1; i <= last; i++) {
        message += arguments[i];
        if (i < last) {
            message += ' ';
        }
    }
    if (MegaLogger && MegaLogger.rootLogger) {
        MegaLogger.rootLogger.error("assertion failed: ", message);
    }
    else if (window.d) {
        console.error(message);
    }

    if (localStorage.stopOnAssertFail) {
        debugger;
    }

    throw new AssertionFailed(message);
}


/**
 * Assert that a user handle is potentially valid (e. g. not an email address).
 *
 * @param userHandle {string}
 *     The user handle to check.
 * @throws
 *     Throws an exception on something that does not seem to be a user handle.
 */
var assertUserHandle = function(userHandle) {
    try {
        if (typeof userHandle !== 'string'
                || base64urldecode(userHandle).length !== 8) {

            throw 1;
        }
    }
    catch (ex) {
        assert(false, 'This seems not to be a user handle: ' + userHandle);
    }
};


/**
 * Pad/prepend `val` with "0" (zeros) until the length is === `length`
 *
 * @param val {String} value to add "0" to
 * @param len {Number} expected length
 * @returns {String}
 */
function addZeroIfLenLessThen(val, len) {
    if (val.toString().length < len) {
        for (var i = val.toString().length; i < len; i++) {
            val = "0" + val;
        }
    }
    return val;
}

function ASSERT(what, msg, udata) {
    if (!what) {
        var af = new Error('failed assertion; ' + msg);
        if (udata) {
            af.udata = udata;
        }
        Soon(function() {
            throw af;
        });
        if (console.assert) {
            console.assert(what, msg);
        }
        else {
            console.error('FAILED ASSERTION', msg);
        }
    }
    return !!what;
}

// log failures through jscrashes system
function srvlog(msg, data, silent) {
    if (data && !(data instanceof Error)) {
        data = {
            udata: data
        };
    }
    if (!silent && d) {
        console.error(msg, data);
    }
    if (typeof window.onerror === 'function') {
        window.onerror(msg, '@srvlog', data ? 1 : -1, 0, data || null);
    }
}

// log failures through event id 99666
function srvlog2(type /*, ...*/) {
    if (d || window.exTimeLeft) {
        var args    = toArray.apply(null, arguments);
        var version = buildVersion.website;

        if (is_extension) {
            if (is_chrome_firefox) {
                version = window.mozMEGAExtensionVersion || buildVersion.firefox;
            }
            else if (is_firefox_web_ext) {
                version = buildVersion.firefox;
            }
            else if (mega.chrome) {
                version = buildVersion.chrome;
            }
            else {
                version = buildVersion.commit && buildVersion.commit.substr(0, 8) || '?';
            }
        }
        args.unshift((is_extension ? 'e' : 'w') + (version || '-'));

        eventlog(99666, JSON.stringify(args));
    }
}

/**
 * Original: http://stackoverflow.com/questions/7317299/regex-matching-list-of-emoticons-of-various-type
 *
 * @param text
 * @returns {XML|string|void}
 * @constructor
 */
function RegExpEscape(text) {
    'use strict';
    return text.replace(/[\s#$()*+,.?[\\\]^{|}-]/g, "\\$&");
}


/**
 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
 *
 * @author <a href="mailto:gary.court.gmail.com">Gary Court</a>
 * @see http://github.com/garycourt/murmurhash-js
 * @author <a href="mailto:aappleby.gmail.com">Austin Appleby</a>
 * @see http://sites.google.com/site/murmurhash/
 *
 * @param {string} key ASCII only
 * @param {number} seed Positive integer only
 * @return {number} 32-bit positive integer hash
 */
function MurmurHash3(key, seed) {
    var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i;

    remainder = key.length & 3; // key.length % 4
    bytes = key.length - remainder;
    h1 = seed || 0xe6546b64;
    c1 = 0xcc9e2d51;
    c2 = 0x1b873593;
    i = 0;

    while (i < bytes) {
        k1 =
            ((key.charCodeAt(i) & 0xff)) |
            ((key.charCodeAt(++i) & 0xff) << 8) |
            ((key.charCodeAt(++i) & 0xff) << 16) |
            ((key.charCodeAt(++i) & 0xff) << 24);
        ++i;

        k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
        k1 = (k1 << 15) | (k1 >>> 17);
        k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;

        h1 ^= k1;
        h1 = (h1 << 13) | (h1 >>> 19);
        h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
        h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
    }

    k1 = 0;

    switch (remainder) {
        case 3:
            k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
        case 2:
            k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
        case 1:
            k1 ^= (key.charCodeAt(i) & 0xff);

            k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
            k1 = (k1 << 15) | (k1 >>> 17);
            k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
            h1 ^= k1;
    }

    h1 ^= key.length;

    h1 ^= h1 >>> 16;
    h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
    h1 ^= h1 >>> 13;
    h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
    h1 ^= h1 >>> 16;

    return h1 >>> 0;
}

/**
 * Ask the user for a decryption key
 * @param {String} ph   The node's handle
 * @param {String} fl   Whether is a folderlink
 * @param {String} keyr If a wrong key was used
 * @return {MegaPromise}
 */
function mKeyDialog(ph, fl, keyr, selector) {
    "use strict";

    var promise = new MegaPromise();
    var $dialog = $(is_mobile ? '#mobile-decryption-key-overlay' : '.mega-dialog.dlkey-dialog');
    var $button = $(is_mobile ? '.mobile.decrypt-button' : '.fm-dialog-new-folder-button', $dialog);
    var $input = $(is_mobile ? '.mobile.decryption-key' : 'input', $dialog);

    if (keyr) {
        $('.mega-dialog.dlkey-dialog .instruction-message')
            .text(l[9048]);
    }
    else {
        $('.mega-dialog.dlkey-dialog input').val('');
        $('.mega-dialog.dlkey-dialog .instruction-message')
            .safeHTML(l[7945] + '<br/>' + l[7972]);
    }

    $('.new-download-buttons').addClass('hidden');
    $('.new-download-file-title').text(l[1199]);
    $('.new-download-file-icon').addClass(fileIcon({
        name: 'unknown.unknown'
    }));

    $button.addClass('disabled').removeClass('active');

    if (is_mobile) {
        fm_showoverlay();
        $dialog.removeClass('hidden');
    }
    else {
        M.safeShowDialog('dlkey-dialog', $dialog);
    }

    const processKeyboardEvent = (evt) => {
        if (evt.key === 'Escape') {
            // Adhering to overlay's behaviour here, blocking the esc click
            evt.preventDefault();
            evt.stopPropagation();
        }
    };

    $('.js-close', $dialog).rebind('click.keydlg', () => {
        loadSubPage('start');
    });

    document.addEventListener('keydown', processKeyboardEvent);

    $dialog.rebind('dialog-closed.dlkey', () => {
        document.removeEventListener('keydown', processKeyboardEvent);
    });

    $input.rebind('input keypress', function(e) {
        var length = String($(this).val() || '').length;

        if (length) {
            $button.removeClass('disabled').addClass('active');
            if (e.keyCode === 13) {
                $button.click();
            }
        }
        else {
            $button.removeClass('active').addClass('disabled');
        }
    });

    $button.rebind('click.keydlg', function() {

        if ($(this).hasClass('active')) {

            // Trim the input from the user for whitespace, newlines etc on either end
            var key = $.trim($input.val());

            if (key) {

                // Remove the !,# from the key which is exported from the export dialog
                key = key.replace('!', '').replace('#', '');

                var newHash = (fl ? '/#F!' : '/#!') + ph + '!' + key;

                var currLink = getSitePath();

                if (isPublickLinkV2(currLink)) {
                    newHash = (fl ? '/folder/' : '/file/') + ph + '#' + key + (selector ? selector : '');
                }

                if (getSitePath() !== newHash) {
                    promise.resolve(key);

                    fm_hideoverlay();
                    $dialog.addClass('hidden');
                    loadSubPage(newHash);

                }
            }
            else {
                promise.reject();
            }
        }
    });

    return promise;
}

function mRandomToken(pfx) {
    'use strict';
    return `${pfx || ''}!${Math.random().toString(28).slice(-9)}`;
}

function str_mtrunc(str, len) {
    if (!len) {
        len = 35;
    }
    if (len > (str || '').length) {
        return str;
    }
    var p1 = Math.ceil(0.60 * len),
        p2 = Math.ceil(0.30 * len);
    return str.substr(0, p1) + '\u2026' + str.substr(-p2);
}

function getTransfersPercent() {
    var dl_r = 0;
    var dl_t = 0;
    var ul_r = 0;
    var ul_t = 0;
    var tp = $.transferprogress || {};
    var zips = {};
    var i;

    for (i = dl_queue.length; i--;) {
        var q = dl_queue[i];
        var td = q && tp[q.zipid ? 'zip_' + q.zipid : 'dl_' + q.id];

        if (td) {
            dl_r += td[0];
            dl_t += td[1];
            if (!q.zipid || !zips[q.zipid]) {
                if (q.zipid) {
                    zips[q.zipid] = 1;
                }
            }
        }
        else {
            dl_t += q && q.size || 0;
        }
    }
    for (i = ul_queue.length; i--;) {
        var tu = tp['ul_' + ul_queue[i].id];

        if (tu) {
            ul_r += tu[0];
            ul_t += tu[1];
        }
        else {
            ul_t += ul_queue[i].size || 0;
        }
    }
    if (dl_t) {
        dl_t += tp['dlc'] || 0;
        dl_r += tp['dlc'] || 0;
    }
    if (ul_t) {
        ul_t += tp['ulc'] || 0;
        ul_r += tp['ulc'] || 0;
    }

    return {
        ul_total: ul_t,
        ul_done: ul_r,
        dl_total: dl_t,
        dl_done: dl_r
    };
}

function percent_megatitle() {
    'use strict';
    var t;
    var transferStatus = getTransfersPercent();

    var x_ul = Math.floor(transferStatus.ul_done / transferStatus.ul_total * 100) || 0;
    var x_dl = Math.floor(transferStatus.dl_done / transferStatus.dl_total * 100) || 0;

    if (transferStatus.dl_total && transferStatus.ul_total) {
        t = ' \u2193 ' + x_dl + '% \u2191 ' + x_ul + '%';
    }
    else if (transferStatus.dl_total) {
        t = ' \u2193 ' + x_dl + '%';
    }
    else if (transferStatus.ul_total) {
        t = ' \u2191 ' + x_ul + '%';
    }
    else {
        t = '';
        $.transferprogress = Object.create(null);
    }
    megatitle(t);

    var d_deg = 360 * x_dl / 100;
    var u_deg = 360 * x_ul / 100;
    var $dl_rchart = $('.transfers .download .nw-fm-chart0.right-c p');
    var $dl_lchart = $('.transfers .download .nw-fm-chart0.left-c p');
    var $ul_rchart = $('.transfers .upload .nw-fm-chart0.right-c p');
    var $ul_lchart = $('.transfers .upload .nw-fm-chart0.left-c p');

    if (d_deg <= 180) {
        $dl_rchart.css('transform', 'rotate(' + d_deg + 'deg)');
        $dl_lchart.css('transform', 'rotate(0deg)');
    }
    else {
        $dl_rchart.css('transform', 'rotate(180deg)');
        $dl_lchart.css('transform', 'rotate(' + (d_deg - 180) + 'deg)');
    }
    if (u_deg <= 180) {
        $ul_rchart.css('transform', 'rotate(' + u_deg + 'deg)');
        $ul_lchart.css('transform', 'rotate(0deg)');
    }
    else {
        $ul_rchart.css('transform', 'rotate(180deg)');
        $ul_lchart.css('transform', 'rotate(' + (u_deg - 180) + 'deg)');
    }
}

function moveCursortoToEnd(el) {

    'use strict';

    const $el = $(el);
    const $scrollBlock = $el.parent('.ps');

    if (typeof el.selectionStart === "number") {
        el.focus();
        el.selectionStart = el.selectionEnd = el.value.length;
    }
    else if (typeof el.createTextRange !== "undefined") {
        el.focus();
        var range = el.createTextRange();
        range.collapse(false);
        range.select();
    }
    $el.trigger('focus');

    if ($scrollBlock.length) {
        $scrollBlock.scrollTop($el.height());
    }
}

function asyncApiReq(data) {
    'use strict';

    // TODO: find&replace all occurences
    return M.req(data);
}

// Returns pixels position of element relative to document (top left corner) OR to the parent (IF the parent and the
// target element are both with position: absolute)
function getHtmlElemPos(elem, n) {
    var xPos = 0;
    var yPos = 0;
    var sl, st, cl, ct;
    var pNode;
    while (elem) {
        pNode = elem.parentNode;
        sl = 0;
        st = 0;
        cl = 0;
        ct = 0;
        if (pNode && pNode.tagName && !/html|body/i.test(pNode.tagName)) {
            if (typeof n === 'undefined') // count this in, except for overflow huge menu
            {
                sl = elem.scrollLeft;
                st = elem.scrollTop;
            }
            cl = elem.clientLeft;
            ct = elem.clientTop;
            xPos += (elem.offsetLeft - sl + cl);
            yPos += (elem.offsetTop - st - ct);
        }
        elem = elem.offsetParent;
    }
    return {
        x: xPos,
        y: yPos
    };
}

/**
 * Detects if Flash is enabled or disabled in the user's browser
 * From http://stackoverflow.com/a/20095467
 * @returns {Boolean}
 */
function flashIsEnabled() {

    var flashEnabled = false;

    try {
        var flashObject = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
        if (flashObject) {
            flashEnabled = true;
        }
    }
    catch (e) {
        if (navigator.mimeTypes
                && (navigator.mimeTypes['application/x-shockwave-flash'] !== undefined)
                && (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin)) {
            flashEnabled = true;
        }
    }

    return flashEnabled;
}

/**
 * Gets the current base URL of the page (protocol + hostname) e.g. If on beta.mega.nz it will return https://beta.mega.nz.
 * If on the browser extension it will return the default https://mega.nz. If on localhost it will return https://mega.nz.
 * This can be used to create external links, for example file downloads https://mega.nz/#!qRN33YbK!o4Z76qDqPbiK2G0I...
 * @returns {String}
 */
function getBaseUrl() {
    return 'https://' + (((location.protocol === 'https:') && location.host) || 'mega.nz');
}

/**
 * Like getBaseUrl(), but suitable for extensions to point to internal resources.
 * This should be the same than `bootstaticpath + urlrootfile` except that may differ
 * from a public entry point (Such as the Firefox extension and its mega: protocol)
 * @returns {string}
 */
function getAppBaseUrl() {
    var l = location;
    var base = (l.origin !== 'null' && l.origin || (l.protocol + '//' + l.hostname));
    if (is_extension) {
        base += l.pathname;
    }
    return base;
}

if (d && location.hostname === 'localhost') {
    // eslint-disable-next-line no-func-assign
    getBaseUrl = function() {
        'use strict';
        return location.origin;
    };
}

/**
 * http://stackoverflow.com/a/16344621/402133
 *
 * @param ms
 * @returns {string}
 */
function ms2Time(ms) {
    var secs = ms / 1000;
    ms = Math.floor(ms % 1000);
    var minutes = secs / 60;
    secs = Math.floor(secs % 60);
    var hours = minutes / 60;
    minutes = Math.floor(minutes % 60);
    hours = Math.floor(hours % 24);
    return hours + ":" + minutes + ":" + secs;
}

function secToDuration(s, sep) {
    var dur = ms2Time(s * 1000).split(":");
    var durStr = "";
    sep = sep || ", ";
    if (!secToDuration.regExp) { //regexp compile cache
        secToDuration.regExp = {};
    }

    if (!secToDuration.regExp[sep]) {
        secToDuration.regExp[sep] = new RegExp("" + sep + "$");
    }

    for (var i = 0; i < dur.length; i++) {
        var v = dur[i];
        if (v === "0") {
            if (durStr.length !== 0 && i !== 0) {
                continue;
            }
            else if (i < 2) {
                continue;
            }
        }

        var transString = false;
        if (i === 0) {
            // hour
            transString = mega.icu.format(l.hour_count, parseInt(v));
        }
        else if (i === 1) {
            // minute
            transString = mega.icu.format(l.minute_count, parseInt(v));
        }
        else if (i === 2) {
            // second
            transString = mega.icu.format(l.second_count, parseInt(v));
        }
        else {
            throw new Error("this should never happen.");
        }

        durStr += transString + sep;
    }

    return durStr.replace(secToDuration.regExp[sep], "");
}

function generateAnonymousReport() {
    var $promise = new MegaPromise();
    var report = {};
    report.ua = navigator.userAgent;
    report.ut = u_type;
    report.pbm = !!window.Incognito;
    report.io = window.dlMethod && dlMethod.name;
    report.sb = +('' + $('script[src*="secureboot"]').attr('src')).split('=').pop();
    report.tp = $.transferprogress;
    if (!megaChatIsReady) {
        report.karereState = '#disabled#';
    }
    else {
        report.numOpenedChats = Object.keys(megaChat.chats).length;
        report.haveRtc = typeof SfuClient !== 'undefined' && SfuClient.platformHasSupport();
    }

    var chatStates = {};
    var userAnonMap = {};
    var userAnonIdx = 0;
    var roomUniqueId = 0;
    var roomUniqueIdMap = {};

    if (megaChatIsReady && megaChat.chats) {
        megaChat.chats.forEach(function (v, k) {
            var participants = v.getParticipants();

            participants.forEach(function (v, k) {
                var cc = M.u[v];
                if (cc && cc.u && !userAnonMap[cc.u]) {
                    userAnonMap[cc.u] = {
                        anonId: userAnonIdx++ + rand(1000),
                        pres: megaChat.getPresence(v)
                    };
                }
                participants[k] = cc && cc.u ? userAnonMap[cc.u] : v;
            });

            var r = {
                'roomState': v.getStateAsText(),
                'roomParticipants': participants
            };

            chatStates[roomUniqueId] = r;
            roomUniqueIdMap[k] = roomUniqueId;
            roomUniqueId++;
        });

        report.chatRoomState = chatStates;
    };

    if (is_chrome_firefox) {
        report.mo = mozBrowserID + '::' + is_chrome_firefox + '::' + mozMEGAExtensionVersion;
    }

    var apireqHaveBackOffs = {};
    apixs.forEach(function(v, k) {
        if (v.backoff > 0) {
            apireqHaveBackOffs[k] = v.backoff;
        }
    });

    if (Object.keys(apireqHaveBackOffs).length > 0) {
        report.apireqbackoffs = apireqHaveBackOffs;
    }

    report.hadLoadedRsaKeys = u_authring.RSA && Object.keys(u_authring.RSA).length > 0;
    report.hadLoadedEd25519Keys = u_authring.Ed25519 && Object.keys(u_authring.Ed25519).length > 0;
    report.totalDomElements = $("*").length;
    report.totalScriptElements = $("script").length;

    report.totalD = Object.keys(M.d).length;
    report.totalU = M.u.size();
    report.totalC = Object.keys(M.c).length;
    report.totalIpc = Object.keys(M.ipc).length;
    report.totalOpc = Object.keys(M.opc).length;
    report.totalPs = Object.keys(M.ps).length;
    report.l = lang;
    report.scrnSize = window.screen.availWidth + "x" + window.screen.availHeight;

    if (typeof window.devicePixelRatio !== 'undefined') {
        report.pixRatio = window.devicePixelRatio;
    }

    try {
        report.perfTiming = JSON.parse(JSON.stringify(window.performance.timing));
        report.memUsed = window.performance.memory.usedJSHeapSize;
        report.memTotal = window.performance.memory.totalJSHeapSize;
        report.memLim = window.performance.memory.jsHeapSizeLimit;
    }
    catch (e) {}

    report.jslC = jslcomplete;
    report.jslI = jsli;
    report.host = window.location.host;

    var promises = [];

    report.version = null; // TODO: how can we find this?

    MegaPromise.allDone(promises)
        .then(function() {
            $promise.resolve(report);
        })
        .catch(function() {
            $promise.resolve(report)
        });

    return $promise;
}

(function(scope) {
    var MegaAnalytics = function(id) {
        this.loggerId = id;
        this.sessionId = makeid(16);
    };
    MegaAnalytics.prototype.log = function(c, e, data) {

        data = data || {};
        data = $.extend(
            true, {}, {
                'aid': this.sessionId,
                'lang': typeof lang !== 'undefined' ? lang : null,
                'browserlang': navigator.language,
                'u_type': typeof u_type !== 'undefined' ? u_type : null
            },
            data
        );

        var msg = JSON.stringify({
            'c': c,
            'e': e,
            'data': data
        });

        if (d) {
            console.log("megaAnalytics: ", c, e, data);
        }
        if (window.location.toString().indexOf("mega.dev") !== -1) {
            return;
        }
        api_req({
            a: 'log',
            e: this.loggerId,
            m: msg
        }, {});
    };
    scope.megaAnalytics = new MegaAnalytics(99999);
})(this);


function constStateToText(enumMap, state) {
    "use strict";
    for (var k in enumMap) {
        if (enumMap[k] === state) {
            return k;
        }
    }
    return "(not found: " + state + ")";
};

/**
 * Helper function that will do some assert()s to guarantee that the new state is correct/allowed
 *
 * @param currentState
 * @param newState
 * @param allowedStatesMap
 * @param enumMap
 * @throws AssertionError
 */
function assertStateChange(currentState, newState, allowedStatesMap, enumMap) {
    "use strict";

    assert(typeof newState !== "undefined", "assertStateChange: newState is 'undefined'");
    var checksAvailable = allowedStatesMap[currentState];
    var allowed = false;
    if (checksAvailable) {
        checksAvailable.forEach(function(allowedState) {
            if (allowedState === newState) {
                allowed = true;
                return false; // break;
            }
        });
    }
    if (!allowed) {
        assert(
            false,
            'State change from: ' + constStateToText(enumMap, currentState) + ' to ' +
            constStateToText(enumMap, newState) + ' is not in the allowed state transitions map.'
        );
    }
}

/**
 * Perform a normal logout
 *
 * @param {Function} aCallback optional
 * @param {Bool} force optional
 */
function mLogout(aCallback, force) {
    "use strict";

    // If user are trying logged out from paid ephemral session, warn user that they cannot get back the paid session.
    if (isNonActivatedAccount()) {
        msgDialog('warninga:!^' + l[78] + '!' + l[79], 'warning', l[23443], l[23444], function(response) {
            if (!response) {
                M.logout();
            }
        });

        return;
    }

    if (!force && mega.ui.passwordReminderDialog) {
        var passwordReminderLogout = mega.ui.passwordReminderDialog.recheckLogoutDialog();

        passwordReminderLogout
            .done(function() {
                mLogout(aCallback, true);
            });

        return;
    }

    var cnt = 0;
    if (M.c[M.RootID] && u_type === 0) {
        for (var i in M.c[M.RootID]) {
            cnt++;
        }
    }
    if (u_type === 0 && cnt > 0) {
        msgDialog('confirmation', l[1057], l[1058], l[1059], function (e) {
            if (e) {
                M.logout();
            }
        });
    }
    else {
        M.logout();
    }
}

// Initialize Rubbish-Bin Cleaning Scheduler
mBroadcaster.addListener('crossTab:master', function _setup() {
    var RUBSCHED_WAITPROC =  20 * 1000;
    var RUBSCHED_IDLETIME =   4 * 1000;
    var timer, updId;

    mBroadcaster.once('crossTab:leave', _exit);

    // The fm must be initialized before proceeding
    if (!folderlink && fminitialized) {
        _fmready();
    }
    else {
        mBroadcaster.addListener('fm:initialized', _fmready);
    }

    function _fmready() {
        if (!folderlink) {
            _init();
            return 0xdead;
        }
    }

    function _update(enabled) {
        _exit();
        if (enabled) {
            _init();
        }
    }

    function _exit() {
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
        if (updId) {
            mBroadcaster.removeListener(updId);
            updId = null;
        }
    }

    function _init() {
        // if (d) console.log('Initializing Rubbish-Bin Cleaning Scheduler');

        // updId = mBroadcaster.addListener('fmconfig:rubsched', _update);
        if (fmconfig.rubsched) {
            timer = setInterval(function() {
                // Do nothing unless the user has been idle
                if (Date.now() - lastactive < RUBSCHED_IDLETIME) {
                    return;
                }
                _exit();

                dbfetch.coll([M.RubbishID]).finally(_proc);

            }, RUBSCHED_WAITPROC);
        }
    }

    function _proc() {

        // Mode 14 - Remove files older than X days
        // Mode 15 - Keep the Rubbish-Bin under X GB
        var mode = String(fmconfig.rubsched).split(':');
        var xval = mode[1];
        mode = +mode[0];

        var handler = _rubSchedHandler[mode];
        if (!handler) {
            throw new Error('Invalid RubSchedHandler', mode);
        }

        if (d) {
            console.log('Running Rubbish Bin Cleaning Scheduler', mode, xval);
            console.time('rubsched');
        }

        // Watch how long this is running
        var startTime = Date.now();

        // Get nodes in the Rubbish-bin
        var nodes = Object.keys(M.c[M.RubbishID] || {});
        var rubnodes = [];

        for (var i = nodes.length; i--; ) {
            var node = M.d[nodes[i]];
            if (!node) {
                console.error('Invalid node', nodes[i]);
                continue;
            }
            rubnodes = rubnodes.concat(M.getNodesSync(node.h, true));
        }

        rubnodes.sort(handler.sort);
        var rNodes = handler.log(rubnodes);

        // if (d) console.log('rubnodes', rubnodes, rNodes);

        var handles = [];
        if (handler.purge(xval)) {
            for (var i in rubnodes) {
                var node = M.d[rubnodes[i]];

                if (handler.remove(node, xval)) {
                    handles.push(node.h);

                    if (handler.ready(node, xval)) {
                        break;
                    }

                    // Abort if this has been running for too long..
                    if ((Date.now() - startTime) > 7000) {
                        break;
                    }
                }
            }

            // if (d) console.log('RubSched-remove', handles);

            if (handles.length) {
                var inRub = (M.RubbishID === M.currentrootid);

                handles.map(function(handle) {
                    // M.delNode(handle, true);    // must not update DB pre-API
                    api_req({a: 'd', n: handle/*, i: requesti*/});

                    if (inRub) {
                        $('.grid-table.fm#' + handle).remove();
                        $('.data-block-view#' + handle).remove();
                    }
                });

                if (inRub) {
                    M.addViewUI();
                }
            }
        }

        if (d) {
            console.timeEnd('rubsched');
        }

        // Once we ran for the first time, set up a long running scheduler
        RUBSCHED_WAITPROC = 4 * 3600 * 1e3;
        _init();
    }

    /**
     * Scheduler Handlers
     *   Sort:    Sort nodes specifically for the handler purpose
     *   Log:     Keep a record of nodes if required and return a debugable array
     *   Purge:   Check whether the Rubbish-Bin should be cleared
     *   Remove:  Return true if the node is suitable to get removed
     *   Ready:   Once a node is removed, check if the criteria has been meet
     */
    var _rubSchedHandler = {
        // Remove files older than X days
        "14": {
            sort: function(n1, n2) {
                return M.d[n1].ts > M.d[n2].ts;
            },
            log: function(nodes) {
                return d && nodes.map(function(node) {
                    return M.d[node].name + '~' + (new Date(M.d[node].ts*1000)).toISOString();
                });
            },
            purge: function(limit) {
                return true;
            },
            remove: function(node, limit) {
                limit = (Date.now() / 1e3) - (limit * 86400);
                return node.ts < limit;
            },
            ready: function(node, limit) {
                return false;
            }
        },
        // Keep the Rubbish-Bin under X GB
        "15": {
            sort: function(n1, n2) {
                n1 = M.d[n1].s || 0;
                n2 = M.d[n2].s || 0;
                return n1 < n2;
            },
            log: function(nodes) {
                var pnodes, size = 0;

                pnodes = nodes.map(function(node) {
                    size += (M.d[node].s || 0);
                    return M.d[node].name + '~' + bytesToSize(M.d[node].s);
                });

                this._size = size;

                return pnodes;
            },
            purge: function(limit) {
                return this._size > (limit * 1024 * 1024 * 1024);
            },
            remove: function(node, limit) {
                return true;
            },
            ready: function(node, limit) {
                this._size -= (node.s || 0);
                return this._size < (limit * 1024 * 1024 * 1024);
            }
        }
    };
});

/** prevent tabnabbing attacks */
mBroadcaster.once('startMega', function() {
    return;

    if (!(window.chrome || window.safari || window.opr)) {
        return;
    }

    // Check whether is safe to open a link through the native window.open
    var isSafeTarget = function(link) {
        link = String(link);

        var allowed = [
            getBaseUrl(),
            getAppBaseUrl()
        ];

        var rv = allowed.some(function(v) {
            return link.indexOf(v) === 0;
        });

        if (d) {
            console.log('isSafeTarget', link, rv);
        }

        return rv || (location.hash.indexOf('fm/chat') === -1);
    };

    var open = window.open;
    delete window.open;

    // Replace the native window.open which will open unsafe links through a hidden iframe
    Object.defineProperty(window, 'open', {
        writable: false,
        enumerable: true,
        value: function(url) {
            var link = document.createElement('a');
            link.href = url;

            if (isSafeTarget(link.href)) {
                return open.apply(window, arguments);
            }

            var iframe = mCreateElement('iframe', {type: 'content', style: 'display:none'}, 'body');
            var data = 'var win=window.open("' + escapeHTML(link) + '");if(win)win.opener = null;';
            var doc = iframe.contentDocument || iframe.contentWindow.document;
            var script = doc.createElement('script');
            script.type = 'text/javascript';
            script.src = mObjectURL([data], script.type);
            script.onload = SoonFc(function() {
                myURL.revokeObjectURL(script.src);
                document.body.removeChild(iframe);
            });
            doc.body.appendChild(script);
        }
    });

    // Catch clicks on links and forward them to window.open
    document.documentElement.addEventListener('click', function(ev) {
        var node = Object(ev.target);

        if (node.nodeName === 'A' && node.href
                && String(node.getAttribute('target')).toLowerCase() === '_blank'
                && !isSafeTarget(node.href)) {

            ev.stopPropagation();
            ev.preventDefault();

            window.open(node.href);
        }
    }, true);
});

/**
 * Simple alias that will return a random number in the range of: a < b
 *
 * @param a {Number} min
 * @param b {Number} max
 * @returns {*}
 */
function rand_range(a, b) {
    return Math.random() * (b - a) + a;
}

/**
 * Invoke the password manager in Chrome.
 *
 * There are some requirements for this function work propertly:
 *
 *  1. The username/password needs to be in a <form/>
 *  2. The form needs to be filled and visible when this function is called
 *  3. After this function is called, within the next second the form needs to be gone
 *
 * As an example take a look at the `tooltiplogin.init()` function in `top-tooltip-login.js`.
 *
 * @param {String|Object} form jQuery selector of the form
 * @return {Boolean} Returns true if the password manager can be called.
 *
 */
function passwordManager(form) {

    'use strict';

    var $form = $(form);

    if ($form.length === 0) {
        return false;
    }

    if (is_chrome_firefox) {
        var creds = passwordManager.pickFormFields(form);
        if (creds) {
            mozRunAsync(mozLoginManager.saveLogin.bind(mozLoginManager, creds.usr, creds.pwd));
        }
        $form.find('input').val('');
        return;
    }
    if (typeof history !== "object") {
        return false;
    }
    $form.rebind('submit', function() {
        setTimeout(function() {
            var path = getSitePath();
            history.replaceState({ success: true }, '', "index.html#" + document.location.hash.substr(1));
            if (hashLogic || isPublicLink(path)) {
                path = path.replace('/', '/#');

                if (is_extension) {
                    path = path.replace('/#', '/' + urlrootfile + '#');
                }
            }
            history.replaceState({ success: true, subpage: getCleanSitePath(path) }, '', path);
            $form.find('input').val('');
        }, 1000);
        return false;
    });

    // For trigger FF Password Manager, submit the form by making submit button and click it.
    var submitButton = document.createElement("input");
    submitButton.setAttribute("type", "submit");
    submitButton.style.opacity = '0';

    $form[0].appendChild(submitButton);

    submitButton.click();

    return true;
}
passwordManager.knownForms = Object.freeze({
    '#form_login_header': {
        usr: '#login-name',
        pwd: '#login-password'
    },
    '#login_form': {
        usr: '#login-name2',
        pwd: '#login-password2'
    },
    '#register_form': {
        usr: '#register-email',
        pwd: '#register-password'
    }
});
passwordManager.pickFormFields = function(form) {
    var result = null;
    var $form = $(form);

    if ($form.length) {
        if ($form.length !== 1) {
            console.error('Unexpected form selector', form);
        }
        else {
            form = passwordManager.knownForms[form];
            if (form) {
                result = {
                    usr: $form.find(form.usr).val(),
                    pwd: $form.find(form.pwd).val(),

                    selector: {
                        usr: form.usr,
                        pwd: form.pwd
                    }
                };

                if (!(result.usr && result.pwd)) {
                    result = false;
                }
            }
        }
    }

    return result;
};

/**
 * Check if the passed in element (DOMNode) is FULLY visible in the viewport.
 *
 * @param el {DOMNode}
 * @returns {boolean}
 */
function elementInViewport(el) {
    if (!verge.inY(el)) {
        return false;
    }
    if (!verge.inX(el)) {
        return false;
    }

    var rect = verge.rectangle(el);

    return !(rect.left < 0 || rect.right < 0 || rect.bottom < 0 || rect.top < 0);
}

/**
 * Check if the element is within the viewport, not detached and visible.
 * @param {HTMLElement|Element} el DOM node/element.
 * @returns {Boolean} whether it is.
 */
function elementIsVisible(el) {
    'use strict';

    if (!(el && el.parentNode && verge.inViewport(el))) {
        return false;
    }

    if (window.getComputedStyle(el).position !== 'fixed' && !el.offsetParent) {
        return false;
    }

    return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}

// FIXME: This is a "Dirty Hack" (TM) that needs to be removed as soon as
//        the original problem is found and resolved.
if (typeof sjcl !== 'undefined') {
    // We need to track SJCL exceptions for ticket #2348
    sjcl.exception.invalid = function(message) {
        this.toString = function() {
            return "INVALID: " + this.message;
        };
        this.message = message;
        this.stack = M.getStack();
    };
}

(function($, scope) {
    /**
     * Share related operations.
     *
     * @param opts {Object}
     *
     * @constructor
     */
    var Share = function(opts) {

        var self = this;
        var defaultOptions = {
        };

        self.options = $.extend(true, {}, defaultOptions, opts);    };

    /**
     * isShareExists
     *
     * Checking if there's available shares for selected nodes.
     * @param {Array} nodes Holds array of ids from selected folders/files (nodes).
     * @param {Boolean} fullShare Do we need info about full share.
     * @param {Boolean} pendingShare Do we need info about pending share .
     * @param {Boolean} linkShare Do we need info about link share 'EXP'.
     * @returns {Boolean} result.
     */
    Share.prototype.isShareExist = function(nodes, fullShare, pendingShare, linkShare) {

        var self = this;

        var shares = {}, length;

        for (var i in nodes) {
            if (nodes.hasOwnProperty(i)) {

                // Look for full share
                if (fullShare) {
                    shares = M.d[nodes[i]] && M.d[nodes[i]].shares;

                    // Look for link share
                    if (linkShare) {
                        if (shares && Object.keys(shares).length) {
                            return true;
                        }
                    }
                    else { // Exclude folder/file links,
                        if (shares) {
                            length = self.getFullSharesNumber(shares);
                            if (length) {
                                return true;
                            }
                        }
                    }
                }

                // Look for pending share
                if (pendingShare) {
                    shares = M.ps[nodes[i]];

                    if (shares && Object.keys(shares).length) {
                        return true;
                    }
                }
            }
        }

        return false;
    };

    /**
     * hasExportLink, check if at least one selected item have public link.
     *
     * @param {String|Array} nodes Node id or array of nodes string
     * @returns {Boolean}
     */
    Share.prototype.hasExportLink = function(nodes) {

        if (typeof nodes === 'string') {
            nodes = [nodes];
        }

        // Loop through all selected items
        for (var i in nodes) {
            var node = M.d[nodes[i]];

            if (node && Object(node.shares).EXP) {
                return true;
            }
        }

        return false;
    };

    /**
     * getFullSharesNumber
     *
     * Loops through all shares and return number of full shares excluding
     * ex. full contacts. Why ex. full contact, in the past when client removes
     * full contact from the list, share related to client remains active on
     * owners side. That behaviour is changed/updated on API side, so now after
     * full contact relationship is removed, related shares are also removed.
     *
     * @param {Object} shares
     * @returns {Integer} result Number of shares
     */
    Share.prototype.getFullSharesNumber = function(shares) {

        var result = 0;
        var contactKeys = [];

        if (shares) {
            contactKeys = Object.keys(shares);
            $.each(contactKeys, function(ind, key) {

                // Count only full contacts
                if (M.u[key] && M.u[key].c) {
                    result++;
                }
            });
        }

        return result;
    };

    /**
     * addContactToFolderShare
     *
     * Add verified email addresses to folder shares.
     */
    Share.prototype.addContactToFolderShare = function addContactToFolderShare() {

        var promise = MegaPromise.resolve();
        var targets = [];
        var $shareDialog = $('.share-dialog');
        var selectedNode;
        var userEmail;
        var permissionLevel;

        // Share button enabled
        if ($.dialog === 'share' && !$('.done-share', $shareDialog).is('.disabled')) {
            selectedNode = $.selected[0];

            // Is there a new contacts planned for addition to share
            if (Object.keys($.addContactsToShare).length > 0) {

                // Add new planned contact to list
                for (var i in $.addContactsToShare) {
                    userEmail = $.addContactsToShare[i].u;
                    permissionLevel = $.addContactsToShare[i].r;

                    if (userEmail && permissionLevel !== undefined) {
                        targets.push({u: userEmail, r: permissionLevel});
                    }
                }
            }

            // Add new contacts to folder share
            if (targets.length > 0) {
                promise = doShare(selectedNode, targets, true);
            }
        }

        return promise;
    };

    Share.prototype.updateNodeShares = function() {

        var self = this;
        var promise = new MegaPromise();

        loadingDialog.show();
        this.removeContactFromShare()
            .always(function() {
                var promises = [];

                if (Object.keys($.changedPermissions).length > 0) {
                    promises.push(doShare($.selected[0], Object.values($.changedPermissions), true));
                }
                promises.push(self.addContactToFolderShare());

                $('.export-links-warning').addClass('hidden');
                console.assert($.dialog === 'share');
                closeDialog();

                MegaPromise.allDone(promises)
                    .always(function() {
                        loadingDialog.hide();
                        promise.resolve.apply(promise, arguments);
                    });
            });

        return promise;
    };


    Share.prototype.removeFromPermissionQueue = function(handle) {
        // Remove the permission change belongs to the specific contact since got removed already
        if ($.changedPermissions && $.changedPermissions[handle]) {
            delete $.changedPermissions[handle];
        }
    };

    Share.prototype.removeContactFromShare = function() {

        var self = this;
        var promises = [];

        if (Object.keys($.removedContactsFromShare).length > 0) {

            Object.values($.removedContactsFromShare).forEach(function(elem) {
                var userEmailOrHandle = elem.userEmailOrHandle;
                var selectedNodeHandle = elem.selectedNodeHandle;
                var userHandle = elem.userHandle;
                var step = 2;
                var packet = false;
                var idtag = mRandomToken('s2');
                var promise = new MegaPromise();
                var resolve = function() {
                    if (!--step) {
                        if (!u_sharekeys[selectedNodeHandle]) {
                            console.error('The share-key should not have been removed...', packet.okd, [packet]);
                        }
                        promise.resolve(packet);
                    }
                };

                promises.push(promise);
                M.scAckQueue[idtag] = requesti;

                // Wait for action-packet acknowledge, this is needed so that removing the last user
                // from a share will issue an `okd` flag which removes the associated sharekey that we
                // have to wait for *if* we're going to re-share to a different user next...
                mBroadcaster.once('share-packet.' + idtag, function(a) {
                    packet = a;
                    resolve();
                });

                // The s2 api call can remove both shares and pending shares
                api_req({
                    a: 's2',
                    n:  selectedNodeHandle,
                    s: [{ u: userEmailOrHandle, r: ''}],
                    ha: '',
                    i: idtag
                }, {
                    userEmailOrHandle: userEmailOrHandle,
                    selectedNodeHandle: selectedNodeHandle,
                    userHandle: userHandle,

                    callback : function(res, ctx) {

                        if (typeof res === 'object') {
                            if (res.r && res.r[0] === ENOENT) {
                                if (d) {
                                    console.error('User %s not found as part of this share.', ctx.userHandle, ctx);
                                }
                                onIdle(() => msgDialog('warninga', l[135], l[47], l[23433]));
                                delete M.scAckQueue[idtag];
                                --step;
                            }

                            // If it was a user handle, the share is a full share
                            if (M.u[ctx.userHandle]) {
                                M.delNodeShare(ctx.selectedNodeHandle, ctx.userHandle);
                                setLastInteractionWith(ctx.userHandle, "0:" + unixtime());

                                self.removeFromPermissionQueue(ctx.userHandle);
                            }
                            // Pending share
                            else {
                                var pendingContactId = M.findOutgoingPendingContactIdByEmail(ctx.userEmailOrHandle);
                                M.deletePendingShare(ctx.selectedNodeHandle, pendingContactId);

                                self.removeFromPermissionQueue(pendingContactId);
                            }

                            resolve();
                        }
                        else {
                            // FIXME: display error to user

                            promise.reject(res);
                        }
                    }
                });
            });
        }

        return MegaPromise.allDone(promises);
    };

    /**
     * Removes any shares (including pending) from the selected node.
     *
     * @returns {MegaPromise} promise to remove contacts from share
     */
    Share.prototype.removeSharesFromSelected = function() {
        'use strict';
        $.removedContactsFromShare = {};
        const nodeHandle = String($.selected[0]);
        let userHandles = M.getNodeShareUsers(nodeHandle, 'EXP');

        if (M.ps[nodeHandle]) {
            const pendingShares = Object(M.ps[nodeHandle]);
            userHandles = [...userHandles, ...Object.keys(pendingShares)];
        }

        for (let i = 0; i < userHandles.length; i++) {
            const userHandle = userHandles[i];
            const userEmailOrHandle = Object(M.opc[userHandle]).m || userHandle;

            $.removedContactsFromShare[userHandle] = {
                'selectedNodeHandle': nodeHandle,
                'userEmailOrHandle': userEmailOrHandle,
                'userHandle': userHandle
            };
        }

        return this.removeContactFromShare();
    };

    // export
    scope.mega.Share = Share;
})(jQuery, window);



(function(scope) {
    /** Utilities for Set operations. */
    scope.setutils = {};

    /**
     * Helper function that will return an intersect Set of two sets given.
     *
     * @private
     * @param {Set} set1
     *     First set to intersect with.
     * @param {Set} set2
     *     Second set to intersect with.
     * @return {Set}
     *     Intersected result set.
     */
    scope.setutils.intersection = function(set1, set2) {

        var result = new Set();
        set1.forEach(function _setIntersectionIterator(item) {
            if (set2.has(item)) {
                result.add(item);
            }
        });

        return result;
    };


    /**
     * Helper function that will return a joined Set of two sets given.
     *
     * @private
     * @param {Set} set1
     *     First set to join with.
     * @param {Set} set2
     *     Second set to join with.
     * @return {Set}
     *     Joined result set.
     */
    scope.setutils.join = function(set1, set2) {

        var result = new Set(set1);
        set2.forEach(function _setJoinIterator(item) {
            result.add(item);
        });

        return result;
    };

    /**
     * Helper function that will return a Set from set1 subtracting set2.
     *
     * @private
     * @param {Set} set1
     *     First set to subtract from.
     * @param {Set} set2
     *     Second set to subtract.
     * @return {Set}
     *     Subtracted result set.
     */
    scope.setutils.subtract = function(set1, set2) {

        var result = new Set(set1);
        set2.forEach(function _setSubtractIterator(item) {
            result.delete(item);
        });

        return result;
    };

    /**
     * Helper function that will compare two Sets for equality.
     *
     * @private
     * @param {Set} set1
     *     First set to compare.
     * @param {Set} set2
     *     Second set to compare.
     * @return {Boolean}
     *     `true` if the sets are equal, `false` otherwise.
     */
    scope.setutils.equal = function(set1, set2) {

        if (set1.size !== set2.size) {
            return false;
        }

        var result = true;
        set1.forEach(function _setEqualityIterator(item) {
            if (!set2.has(item)) {
                result = false;
            }
        });

        return result;
    };
})(window);

/**
 * Transoms the numerical preferences to preferences view object
 * @param {Number} pref     Integer value representing the preferences
 * @returns {Object}        View preferences object
 */
function getFMColPrefs(pref) {
    'use strict';
    if (pref === undefined) {
        return;
    }
    var columnsPreferences = Object.create(null);
    columnsPreferences.fav = pref & 4;
    columnsPreferences.label = pref & 1;
    columnsPreferences.size = pref & 8;
    columnsPreferences.type = pref & 64;
    columnsPreferences.timeAd = pref & 32;
    columnsPreferences.timeMd = pref & 16;
    columnsPreferences.versions = pref & 2;
    columnsPreferences.playtime = pref & 128;

    return columnsPreferences;
}

/**
 * Get the number needed for bitwise operator
 * @param {String} colName      Column name
 * @returns {Number}            Number to be used in bitwise operator
 */
function getNumberColPrefs(colName) {
    'use strict';
    switch (colName) {
        case 'fav': return 4;
        case 'label': return 1;
        case 'size': return 8;
        case 'type': return 64;
        case 'timeAd': return 32;
        case 'timeMd': return 16;
        case 'versions': return 2;
        case 'playtime': return 128;
        default: return null;
    }
}

function invalidLinkError() {
    'use strict';
    loadingInitDialog.hide();

    loadfm.loaded = false;
    loadfm.loading = false;
    if (!is_mobile) {
        var title = l[8531];
        var message = l[17557];
        msgDialog('warninga', title, message, false, function () {
            // If the user is logged-in, he'll be redirected to the cloud
            loadSubPage('login');
        });
    }
    else {
        // Show file/folder not found overlay
        mobile.notFoundOverlay.show();
    }
}

/**
 * Classifies the strength of the password (Mainly used on the MegaInputs)
 * ZXCVBN library need to be inited before executing this function.
 * The minimum allowed strength is 8 characters in length and password score of 1 (weak).
 * @param {String} password The user's password (should be trimmed for whitespace beforehand)
 */
function classifyPassword(password) {

    'use strict';

    if (typeof zxcvbn !== 'function') {
        onIdle(() => {
            throw new Error('zxcvbn init fault');
        });
        console.error('zxcvbn is not inited');
        return false;
    }

    // Calculate the password score using the ZXCVBN library and its length
    password = $.trim(password);
    var passwordScore = zxcvbn(password).score;
    var passwordLength = password.length;
    var result = {};

    if (passwordLength === 0) {
        return false;
    }
    else if (passwordLength < security.minPasswordLength) {
        result = {
            string1: l[18700],
            string2: l[18701],                      // Your password needs to be at least 8 characters long
            className: 'good1',                     // Very weak
            statusClass: 'insufficient-strength'
        };
    }
    else if (passwordScore === 4) {
        result = {
            string1: l[1128],
            string2: l[1123],
            className: 'good5',                     // Strong
            statusClass: 'meets-minimum-strength'
        };
    }
    else if (passwordScore === 3) {
        result = {
            string1: l[1127],
            string2: l[1122],
            className: 'good4',                     // Good
            statusClass: 'meets-minimum-strength'
        };
    }
    else if (passwordScore === 2) {
        result = {
            string1: l[1126],
            string2: l[1121],
            className: 'good3',                     // Medium
            statusClass: 'meets-minimum-strength'
        };
    }
    else if (passwordScore === 1) {
        result = {
            string1: l[1125],
            string2: l[1120],
            className: 'good2',                     // Weak
            statusClass: 'meets-minimum-strength'
        };
    }
    else {
        result = {
            string1: l[1124],
            string2: l[1119],
            className: 'good1',                     // Very weak
            statusClass: 'insufficient-strength'
        };
    }

    return result;
}

/**
 * A function to get the last day of the month
 * @param {Date} dateObj        The Date for which to return the last day of the month
 * @returns {Date}              the result date object with the last day of the month
 */
function getLastDayofTheMonth(dateObj) {
    "use strict";
    if (!dateObj) {
        return null;
    }

    var day;
    var month = dateObj.getMonth();
    var year = dateObj.getFullYear();
    if ([0, 2, 4, 6, 7, 9, 11].indexOf(month) >= 0) {
        day = 31;
    }
    else if (month === 1) {
        if (year % 4 !== 0) {
            day = 28;
        }
        else if (year % 100 !== 0) {
            day = 29;
        }
        else if (year % 400 !== 0) {
            day = 28;
        }
        else {
            day = 29;
        }
    }
    else {
        day = 30;
    }
    return new Date(year, month, day);
}

/**
 * Block Chrome Password manager for password field with attribute `autocomplete="new-password"`
 */
function blockChromePasswordManager() {

    "use strict";

    if (window.chrome) {
        var $newPasswordField = $('input[type="password"][autocomplete="new-password"]');
        var switchReadonly = function __switchReadonly(input) {

            input.setAttribute('readonly', true);
            onIdle(function() {
                input.removeAttribute('readonly');
            });
        };

        $newPasswordField.rebind('focus.blockAutofill mousedown.blockAutofill', function() {
            switchReadonly(this);
        });

        // For prevent last chracter deletion pops up password manager
        $newPasswordField.rebind('keydown.blockAutofill', function(e) {

            if ((e.keyCode === 8 &&
                ((this.selectionStart === 1 && this.selectionEnd === 1) ||
                (this.selectionStart === 0 && this.selectionEnd === this.value.length))) ||
                (e.keyCode === 46 &&
                ((this.selectionStart === 0 && this.selectionEnd === 0 && this.value.length === 1) ||
                (this.selectionStart === 0 && this.selectionEnd === this.value.length)))) {
                e.preventDefault();
                this.value = '';
            }
        });
    }
}

/**
 * Attach the download file link handler
 * Use in /desktop and /cmd
 * @param $links
 */
/*exported registerLinuxDownloadButton */
function registerLinuxDownloadButton($links) {
    'use strict';
    $links.rebind('click', function() {
        var $link = $(this);
        if (!$link.hasClass('disabled') && $link.attr('data-link')) {
            window.location = $link.attr('data-link');
        }
        return false;
    });
}
/* eslint-disable complexity */
/**
 * Function that takes users attributes, then prepare content texts of ODQ paywall dialog
 * @param {Object} user_attr        u_attr or {}
 * @param {Object} accountData      M.account, caller must populate then pass
 * @returns {Object}                contains {dialogText, dlgFooterText,fmBannerText}
 */
function odqPaywallDialogTexts(user_attr, accountData) {
    'use strict';

    var totalFiles = (accountData.stats[M.RootID] ? accountData.stats[M.RootID].files : 0) +
        (accountData.stats[M.RubbishID] ? accountData.stats[M.RubbishID].files : 0) +
        (accountData.stats[M.InboxID] ? accountData.stats[M.InboxID].files : 0);

    var dialogText = mega.icu.format(l[23525], totalFiles);
    var dlgFooterText = l[23524];
    var fmBannerText = l[23534];

    if (user_attr.uspw) {
        if (user_attr.uspw.dl) {
            var deadline = new Date(user_attr.uspw.dl * 1000);
            var currDate = new Date();
            var remainDays = Math.floor((deadline - currDate) / 864e5);
            var remainHours = Math.floor((deadline - currDate) / 36e5);

            const sanitiseString = (string) => {
                return escapeHTML(string)
                    .replace(/\[S]/g, '<span>')
                    .replace(/\[\/S]/g, '</span>')
                    .replace(/\[A]/g, '<a href="/pro" class="clickurl">')
                    .replace(/\[\/A]/g, '</a>');
            };
            if (remainDays > 0) {
                dlgFooterText = sanitiseString(mega.icu.format(l.dialog_exceed_storage_quota, remainDays));
                fmBannerText = sanitiseString(mega.icu.format(l.fm_banner_exceed_storage_quota, remainDays));
            }
            else if (remainDays === 0 && remainHours > 0) {
                dlgFooterText = sanitiseString(mega.icu.format(l.dialog_exceed_storage_quota_hours, remainHours));
            }
        }
        if (user_attr.uspw.wts && user_attr.uspw.wts.length) {
            dialogText = mega.icu.format(l[23520], totalFiles);
            if (user_attr.uspw.wts.length === 1) {
                dialogText = mega.icu.format(l[23530], totalFiles);
                dialogText = dialogText.replace('%2', time2date(user_attr.uspw.wts[0], 1));
            }
            if (user_attr.uspw.wts.length === 2) {
                dialogText = dialogText.replace('%2', time2date(user_attr.uspw.wts[0], 1)).replace('%3', '')
                    .replace('%4', time2date(user_attr.uspw.wts[1], 1));
            }
            else if (user_attr.uspw.wts.length === 3) {
                dialogText = dialogText.replace('%2', time2date(user_attr.uspw.wts[0], 1))
                    .replace('%3', time2date(user_attr.uspw.wts[1], 1))
                    .replace('%4', time2date(user_attr.uspw.wts[2], 1));
            }
            else {
                // more than 3
                var datesString = time2date(user_attr.uspw.wts[1], 1);
                for (var k = 2; k < user_attr.uspw.wts.length - 1; k++) {
                    datesString += ', ' + time2date(user_attr.uspw.wts[k], 1);
                }

                dialogText = dialogText.replace('%2', time2date(user_attr.uspw.wts[0], 1))
                    .replace('%3', datesString)
                    .replace('%4', time2date(user_attr.uspw.wts[user_attr.uspw.wts.length - 1], 1));
            }
        }
    }

    var filesText = l[23253]; // 0 files

    dialogText = dialogText.replace('%1', user_attr.email || ' ');
    dialogText = dialogText.replace('%6', bytesToSize(accountData.space_used));

    // In here, it's guaranteed that we have pro.membershipPlans,
    // but we will check for error free logic in case of changes
    var minPlanId = -1;
    var neededPro = 4;
    if (pro.membershipPlans && pro.membershipPlans.length) {
        var spaceUsedGB = accountData.space_used / 1073741824; // = 1024*1024*1024
        var minPlan = 9000000;
        for (var h = 0; h < pro.membershipPlans.length; h++) {
            if (pro.membershipPlans[h][4] === 1 && pro.membershipPlans[h][2] > spaceUsedGB &&
                pro.membershipPlans[h][2] < minPlan) {
                minPlan = pro.membershipPlans[h][2];
                minPlanId = pro.membershipPlans[h][1];
            }
        }
    }
    if (minPlanId === -1) {
        // weirdly, we dont have plans loaded, or no plan matched the storage.
        if (user_attr.p) {
            neededPro = user_attr.p + 1;
            if (neededPro === 3) {
                neededPro = 100;
            }
            else if (neededPro === 5) {
                neededPro = 1;
            }
        }
    }
    else {
        neededPro = minPlanId;
    }

    dialogText = dialogText.replace('%7', pro.getProPlanName(neededPro));

    return {
        dialogText: dialogText,
        dlgFooterText: dlgFooterText,
        fmBannerText: fmBannerText
    };
}


function getTaxName(countryCode) {
    'use strict';
    switch (countryCode) {
        case "AT": return "USt";
        case "BE": return "TVA";
        case "HR": return "PDV";
        case "CZ": return "DPH";
        case "DK": return "moms";
        case "EE": return "km";
        case "FI": return "ALV";
        case "FR": return "TVA";
        case "DE": return "USt";
        case "HU": return "AFA";
        case "IT": return "IVA";
        case "LV": return "PVN";
        case "LT": return "PVM";
        case "LU": return "TVA";
        case "NL": return "BTW";
        case "PL": return "PTU";
        case "PT": return "IVA";
        case "RO": return "TVA";
        case "SK": return "DPH";
        case "SI": return "DDV";
        case "SE": return "MOMS";
        case "AL": return "TVSH";
        case "AD": return "IGI";
        case "AR": return "IVA";
        case "AM": return "AAH";
        case "AU": return "GST";
        case "BO": return "IVA";
        case "BA": return "PDV";
        case "BR": return "ICMS";
        case "CA": return "GST";
        case "CL": return "IVA";
        case "CO": return "IVA";
        case "DO": return "ITBIS";
        case "EC": return "IVA";
        case "SV": return "IVA";
        case "FO": return "MVG";
        case "GT": return "IVA";
        case "IS": return "VSK";
        case "ID": return "PPN";
        case "JE": return "GST";
        case "JO": return "GST";
        case "LB": return "TVA";
        case "LI": return "MWST";
        case "MK": return "DDV";
        case "MY": return "GST";
        case "MV": return "GST";
        case "MX": return "IVA";
        case "MD": return "TVA";
        case "MC": return "TVA";
        case "ME": return "PDV";
        case "MA": return "GST";
        case "NZ": return "GST";
        case "NO": return "MVA";
        case "PK": return "GST";
        case "PA": return "ITBMS";
        case "PY": return "IVA";
        case "PE": return "IGV";
        case "PH": return "RVAT";
        case "RU": return "NDS";
        case "SG": return "GST";
        case "CH": return "MWST";
        case "TN": return "TVA";
        case "TR": return "KDV";
        case "UA": return "PDV";
        case "UY": return "IVA";
        case "UZ": return "QQS";
        case "VN": return "GTGT";
        case "VE": return "IVA";
        case "ES": return "NIF";

        default: return "VAT";
    }
}
/* eslint-enable complexity */

/**
 * Validate entered address is on correct structure, if there is more type of bitcoin structure please update.
 * Reference - https://stackoverflow.com/a/59756959
 * Use in Referral program redemption
 * @param {String} address Bitcoin address
 *
 * @returns {Boolean} result Validity of entered address
 */
function validateBitcoinAddress(address) {

    'use strict';

    return address.match(/(^[13][\1-9A-HJ-NP-Za-km-z]{25,34}$)|(^(bc1)[\dA-HJ-NP-Za-z]{8,87}$)/) === null;
}

(function __fmconfig_handler() {
    "use strict";

    let timer;
    const ns = Object.create(null);
    const privy = Object.create(null);
    const oHasOwn = {}.hasOwnProperty;
    const hasOwn = (o, p) => oHasOwn.call(o, p);
    const oLen = (o) => Object.keys(o || {}).length;
    const parse = tryCatch(v => JSON.parse(v), false);
    const stringify = tryCatch(v => JSON.stringify(v));
    const logger = MegaLogger.getLogger('fmconfig');
    const MMH_SEED = 0x7fee1ef;

    /** @property privy.ht */
    lazy(privy, 'ht', () => {
        return MurmurHash3('ht!' + u_handle + base64urldecode(u_handle), MMH_SEED).toString(16);
    });

    /**
     * Move former/legacy settings stored in localStorage
     * @private
     */
    const moveLegacySettings = function() {
        const prefs = [
            'agreedToCopyrightWarning', 'dl_maxSlots', 'font_size',
            'leftPaneWidth', 'mobileGridViewModeEnabled', 'ul_maxSlots', 'ul_maxSpeed'
        ];
        const replacements = {
            'agreedToCopyrightWarning': 'cws',
            'mobileGridViewModeEnabled': 'mgvm'
        };

        for (let i = prefs.length; i--;) {
            const pref = prefs[i];

            if (localStorage[pref] !== undefined) {
                const p = replacements[pref] || pref;

                if (fmconfig[p] === undefined) {
                    mega.config.set(p, parseInt(localStorage[pref]) | 0);
                }
            }
        }
    };

    // shrink suitable fmconfig settings
    const shrink = (cfg) => {
        if (d) {
            console.time('fmconfig.shrink');
        }

        // eslint-disable-next-line guard-for-in
        for (let slot in shrink.bitdef) {
            let bit = 0;
            let def = shrink.bitdef[slot];

            for (let i = def.length; i--;) {
                const k = def[i];
                const v = 1 << i;

                if (cfg[k] !== undefined) {
                    if (parse(cfg[k]) | 0) {
                        bit |= v;
                    }
                    delete cfg[k];
                }
            }

            cfg[slot] = stringify(bit);
        }

        cfg.xs1 = stringify(
            (cfg.obVer & 0xfff) << 20 | (cfg.font_size & 15) << 12 | cfg.leftPaneWidth & 0xfff
        );
        delete cfg.font_size;
        delete cfg.leftPaneWidth;
        delete cfg.obVer;

        let s = cfg.ul_maxSpeed;
        s = s / 1024 << 1 | (s < 0 ? 1 : 0);
        cfg.xs2 = stringify((s & 0xfffff) << 8 | (cfg.ul_maxSlots & 15) << 4 | cfg.dl_maxSlots & 15);
        delete cfg.ul_maxSpeed;
        delete cfg.ul_maxSlots;
        delete cfg.dl_maxSlots;

        if (cfg.viewercfg) {
            const xs3 = parse(cfg.viewercfg) || {};
            cfg.xs3 = stringify(xs3.speed << 16 | xs3.order << 8 | xs3.repeat << 1 | xs3.sub);
            delete cfg.viewercfg;
        }

        if (cfg.treenodes) {
            cfg.xtn = shrink.tree(cfg.treenodes);
            delete cfg.treenodes;
        }

        if (cfg.viewmodes) {
            cfg.xvm = shrink.views(cfg.viewmodes);
            delete cfg.viewmodes;
        }
        shrink.sorta(cfg);

        if (d) {
            console.timeEnd('fmconfig.shrink');
        }
        return cfg;
    };

    shrink.bitdef = Object.assign(Object.create(null), {
        v04: ['rvonbrddl', 'rvonbrdfd', 'rvonbrdas'],
        obv4: [
            'obcd', 'obcduf', 'obcdmyf', 'obcdda', 'obmc', 'obmclp', 'obmccp', 'obmcmp',
            'obmcco', 'obmcnw', 'obrev'
        ],
        xb1: [
            // do NOT change the order, add new entries at the tail UP TO 31, and 8 per row.
            'cws', 'ctt', 'viewmode', 'dbDropOnLogout', 'dlThroughMEGAsync', 'sdss', 'tpp', 'ulddd',
            'cbvm', 'mgvm', 'uiviewmode', 'uisorting', 'uidateformat', 'skipsmsbanner', 'skipDelWarning', 'rsv1',
            'nowarnpl', 'zip64n', 'callemptytout', 'callinout', 'showHideChat', 'showRecents', 'nocallsup'
        ]
    });

    shrink.tree = (nodes) => {
        let v = '';
        const tn = Object.keys(parse(nodes) || {});
        const pfx = {'o': '1', 'p': '2'};

        for (let i = 0; i < tn.length; ++i) {
            const k = tn[i];
            v += (k[2] === '_' && pfx[k[0]] || '0') + base64urldecode(k.substr(-8));
        }
        return v;
    };

    shrink.views = (nodes) => {
        let r = '';
        const v = parse(nodes);
        const s = Object.keys(v || {});

        for (let i = 0; i < s.length; ++i) {
            const h = s[i];
            const n = (h.length === 8 || h.length === 11) | 0;
            const j = n ? base64urldecode(h) : h;

            r += String.fromCharCode(j.length << 2 | (v[h] & 1) << 1 | n) + j;
        }

        return r;
    };

    shrink.sorta = (config) => {
        const tsort = config.sorting && parse(config.sorting);
        const rules = shrink.sorta.rules = shrink.sorta.rules || Object.keys(M.sortRules || {});
        const shift = o => rules.indexOf(o.n) << 1 | (o.d < 0 ? 1 : 0);
        const store = n => String.fromCharCode(n);
        let res = store(tsort ? shift(tsort) : 0);

        if (config.sortmodes) {
            const sm = Object.assign(Object.create(null), parse(config.sortmodes));

            // eslint-disable-next-line guard-for-in
            for (let h in sm) {
                const v = sm[h];
                const n = (h.length === 8 || h.length === 11) | 0;
                const p = n ? base64urldecode(h) : h;

                if (!rules.includes(v.n)) {
                    logger.warn(`Invalid sort-mode for ${h} %o`, v);
                    continue;
                }

                res += store(shift(v)) + store(p.length << 1 | n) + p;
            }
        }

        config.xsm = res;
        delete config.sorting;
        delete config.sortmodes;
    };
    shrink.sorta.rules = null;

    // stretch previously shrunk settings
    const stretch = (config) => {
        if (d) {
            console.time('fmconfig.stretch');
        }

        // eslint-disable-next-line guard-for-in
        for (let slot in shrink.bitdef) {
            let def = shrink.bitdef[slot];

            for (let i = def.length; i--;) {
                const k = def[i];
                const v = 1 << i;

                if (config[slot] & v) {
                    config[k] = 1;
                }
            }
        }

        if (config.xs1) {
            config.font_size = config.xs1 >> 12 & 15;
            config.leftPaneWidth = config.xs1 & 0xfff;
            config.obVer = config.xs1 >> 20;
        }

        if (config.xs2) {
            let s = config.xs2 >> 8;
            config.dl_maxSlots = config.xs2 & 15;
            config.ul_maxSlots = config.xs2 >> 4 & 15;
            config.ul_maxSpeed = s & 1 ? -1 : (s >> 1) * 1024;
        }

        if (config.xs3) {
            config.viewercfg = {};
            config.viewercfg.speed = config.xs3 >> 16 & 0xFF;
            config.viewercfg.order = config.xs3 >> 8 & 0xFF;
            config.viewercfg.repeat = config.xs3 >> 1 & 1;
            config.viewercfg.sub = config.xs3 & 1;
        }

        if (config.xtn) {
            config.treenodes = stretch.tree(config.xtn);
            delete config.xtn;
        }

        if (config.xvm) {
            config.viewmodes = stretch.views(config.xvm);
            delete config.xvm;
        }

        if (config.xsm) {
            stretch.sorta(config);
        }

        if (d) {
            console.timeEnd('fmconfig.stretch');
        }
        return config;
    };

    stretch.tree = (xtn) => {
        const t = {};
        const p = {'0': '', '1': 'os_', '2': 'pl_'};

        for (let i = 0; i < xtn.length; i += 7) {
            t[p[xtn[i]] + base64urlencode(xtn.substr(i + 1, 6))] = 1;
        }
        return t;
    };

    stretch.views = (xvm) => {
        const v = {};

        for (let i = 0; i < xvm.length;) {
            let b = xvm.charCodeAt(i);
            let l = b >> 2;
            let h = xvm.substr(i + 1, l);

            v[b & 1 ? base64urlencode(h) : h] = b >> 1 & 1;

            i += ++l;
        }
        return v;
    };

    stretch.sorta = (config) => {
        let tmp;
        let xsm = config.xsm;
        const rules = shrink.sorta.rules = shrink.sorta.rules || Object.keys(M.sortRules || {});

        tmp = xsm.charCodeAt(0);
        config.sorting = {n: rules[tmp >> 1], d: tmp & 1 ? -1 : 1};

        tmp = {};
        for (let i = 1; i < xsm.length;) {
            const a = xsm.charCodeAt(i);
            const b = xsm.charCodeAt(i + 1);
            const h = xsm.substr(i + 2, b >> 1);

            tmp[b & 1 ? base64urlencode(h) : h] = {n: rules[a >> 1], d: a & 1 ? -1 : 1};
            i += 2 + (b >> 1);
        }

        delete config.xsm;
        config.sortmodes = tmp;
    };

    // sanitize fmconfig
    const filter = async(fmconfig) => {
        const config = {};
        const nodeType = {viewmodes: 1, sortmodes: 1, treenodes: 1};
        const nTreeFilter = await filter.tree(fmconfig, nodeType);

        for (let key in fmconfig) {
            if (hasOwn(fmconfig, key)) {
                let value = fmconfig[key];

                if (!value && value !== 0) {
                    logger.info('Skipping empty value for "%s"', key);
                    continue;
                }

                // Dont save no longer existing nodes
                if (nodeType[key]) {
                    if (typeof value !== 'object') {
                        logger.warn('Unexpected type for ' + key);
                        continue;
                    }
                    value = nTreeFilter(value);
                }

                if (typeof value === 'object' && !oLen(value)) {
                    logger.info('Skipping empty object "%s"', key);
                    continue;
                }

                config[key] = typeof value === 'string' ? value : stringify(value);

                if (d) {
                    console.assert(config[key] && config[key].length > 0);
                }
            }
        }

        return shrink(config);
    };

    // get node tree sanitizer.
    filter.tree = async(config, types) => {
        const echo = v => v;
        if (pfid) {
            // @todo LRU cache?
            return echo;
        }

        const handles = array.unique(
            Object.keys(types)
                .reduce((s, v) => Object.keys(config[v] || {})
                    .map(h => M.isCustomView(h).nodeID || h).concat(s), [])
                .filter(s => s.length === 8 && s !== 'contacts')
        );

        if (handles.length < 200) {
            return echo;
        }

        const nodes = await dbfetch.node(handles).catch(nop) || [];
        for (let i = nodes.length; i--;) {
            nodes[nodes[i].h] = true;
        }

        const isValid = (handle) => {
            const cv = M.isCustomView(handle);
            handle = cv.nodeID || handle;
            return handle.length !== 8 || nodes[handle] || handle === 'contacts' || handle === cv.type;
        };

        return (tree) => {
            const result = {};
            for (let handle in tree) {
                if (hasOwn(tree, handle)
                    && handle.substr(0, 7) !== 'search/'
                    && isValid(handle)) {

                    result[handle] = tree[handle];
                }
                else {
                    logger.info('Skipping non-existing node "%s"', handle);
                }
            }
            return result;
        };
    };

    // Save fmconfig into WebStorage.
    const saveLocally = async() => {
        let storage = localStorage;

        /**
        if ('csp' in window) {
            await csp.init();

            if (!csp.has('pref')) {
                storage = sessionStorage;
            }
        }
        /**/

        const config = await filter(fmconfig).catch(dump);

        tryCatch(data => {
            data = JSON.stringify(data);
            if (data.length > 262144) {
                logger.warn('fmconfig became larger than 256KB', data.length);
            }
            storage.fmconfig = data;
        }, ex => {
            if (ex.name === 'QuotaExceededError') {
                if (d) {
                    console.warn('WebStorage exhausted!', [fmconfig], stringify(storage).length);
                }

                if (!u_type) {
                    // The user is not logged/registered, let's just expunge it...
                    console.info('Cleaning fmconfig... (%s bytes)', String(storage.fmconfig).length);
                    delete storage.fmconfig;
                }
            }
        })(config);
    };

    /**
     * Pick the global `fmconfig` and sanitize it before
     * sending it to the server, as per TLV requirements.
     * @private
     */
    const store = mutex('fmconfig:store.mutex', async(resolve) => {
        if (!window.u_handle) {
            throw new Error('Unable to store fmconfig in the current context.');
        }

        const exit = (rc, message) => {
            if (d) {
                console.timeEnd('fmconfig.store');
                if (message) {
                    logger.debug(message);
                }
            }
            resolve(rc);
            return rc;
        };

        let str;
        let len;
        const fmconfig = window.fmconfig || {};

        if (d) {
            str = stringify(fmconfig);
            len = str.length;
            console.time('fmconfig.store');
            logger.debug('fmconfig.store:begin (%d bytes)', len, str);
        }

        const config = await filter(fmconfig).catch(dump);
        if (typeof config !== 'object' || !oLen(config)) {
            return exit(ENOENT, 'Not saving fmconfig, invalid...');
        }
        str = stringify(config).replace(/\\u.{4}/g, n => parseInt(n.substr(2), 16));

        if (d) {
            logger.debug('fmconfig.store:end (%d bytes saved)', len - str.length, str);
        }

        len = str.length;

        if (len < 8) {
            return exit(EARGS, 'Not saving fmconfig, data too short...');
        }
        if (len > 12000) {
            return exit(EOVERQUOTA, 'Not saving fmconfig, data exceeds maximum allowed...');
        }

        // generate checksum/hash for the config
        const hash = MurmurHash3(str, MMH_SEED);
        const tag = 'fmc!' + privy.ht;

        // dont store it unless it has changed
        if (hash === localStorage[tag] >>> 0) {
            return exit(EEXIST, 'Not saving fmconfig, unchanged...');
        }

        // fmconfig may changed in our side, but not in server, check it.
        let attr = await Promise.resolve(mega.attr.get(u_handle, 'fmconfig', false, true)).catch(nop);
        if (stringify(attr) === stringify(config)) {
            logger.debug('remote syncing completed.', attr);
        }
        else {
            attr = mega.attr.set('fmconfig', config, false, true);
        }

        localStorage[tag] = hash;
        const promise = Promise.resolve(attr).catch(nop);

        timer = promise;
        promise.then(function() {
            timer = 0;
        });

        return exit(await promise);
    });

    // issue fmconfig persistence upon change.
    const push = () => {
        if (u_type > 2) {
            // through a timer to prevent floods
            timer = delay('fmconfig:store', store, 2600);
        }
        else {
            timer = null;
            delay('fmconfig:store', saveLocally, 3401);
        }
    };

    // Real-time update upon fmconfig syncing.
    const refresh = () => {
        if (fminitialized) {
            refresh.ui();
        }

        if (fmconfig.ul_maxSlots) {
            ulQueue.setSize(fmconfig.ul_maxSlots);
        }

        // quick&dirty(tm) hack, change me whenever we rewrite the underlying logic..
        let dlSlots = $.tapioca ? 1 : fmconfig.dl_maxSlots;
        if (dlSlots) {
            dlQueue.setSize(dlSlots);
        }

        if (fmconfig.font_size && !document.body.classList.contains('fontsize' + fmconfig.font_size)) {
            document.body.classList.remove('fontsize1', 'fontsize2');
            document.body.classList.add('fontsize' + fmconfig.font_size);
        }

        if (fmconfig.fmColPrefs) {
            const prefs = getFMColPrefs(fmconfig.fmColPrefs);
            for (let colPref in prefs) {
                if (hasOwn(prefs, colPref)) {
                    M.columnsWidth.cloud[colPref].viewed = prefs[colPref] > 0;
                }
            }

            if (M.currentrootid === M.RubbishID) {
                M.columnsWidth.cloud.fav.disabled = true;
                M.columnsWidth.cloud.fav.viewed = false;
            }
        }
    };

    refresh.ui = () => {
        if (M.recentsRender && M.recentsRender.hasConfigChanged()) {
            M.recentsRender.onConfigChange();
            if (page.includes('fm/recents')) {
                openRecents();
            }
        }

        if (M.account && page.indexOf('fm/account') > -1) {
            if (!is_mobile) {
                accountUI.renderAccountPage(M.account);
            }
            else if (page === 'fm/account/notifications') {
                mobile.account.notifications.render();
            }
            else if (page === 'fm/account/file-management') {
                mobile.account.filemanagement.render();
            }

            return;
        }

        const view = Object(fmconfig.viewmodes)[M.currentdirid];
        const sort = Object(fmconfig.sortmodes)[M.currentdirid];

        if (view !== undefined && M.viewmode !== view
            || sort !== undefined && (sort.n !== M.sortmode.n || sort.d !== M.sortmode.d)) {

            M.openFolder(M.currentdirid, true);
        }

        if (M.currentrootid === M.RootID) {
            const tree = Object(fmconfig.treenodes);

            if (stringify(tree) !== M.treenodes) {

                M.renderTree();
            }
        }
    };

    // @private
    const define = (target, key, value) => {

        if (d) {
            if (value === undefined) {
                logger.debug('Removing "%s"', key);
            }
            else {
                logger.debug('Setting value for key "%s"', key, value);

                if (String(stringify(value)).length > 1024) {
                    logger.warn('Attempting to store more than 1KB for %s...', key);
                }

                console.assert(typeof value !== 'boolean', 'Invalid value type for ' + key);
                console.assert(typeof key === 'string' && /^\w{2,17}$/.test(key), 'Invalid key ' + key);
            }
        }

        const rc = value === undefined ? Reflect.deleteProperty(target, key) : Reflect.set(target, key, value);

        if (fminitialized) {
            queueMicrotask(push);
        }
        else if (timer !== -MMH_SEED) {
            timer = -MMH_SEED;
            mBroadcaster.once('fm:initialized', push);
        }

        mBroadcaster.sendMessage('fmconfig:' + key, value);

        return rc;
    };

    // Initialize fmconfig.
    const setup = (config) => {
        config = stretch(Object.assign(Object.create(null), config));

        delete window.fmconfig;
        Object.defineProperty(window, 'fmconfig', {
            configurable: true,
            value: new Proxy(config, {
                set(target, prop, value) {
                    return define(target, prop, value);
                },
                deleteProperty(target, prop) {
                    return define(target, prop, undefined);
                }
            })
        });

        moveLegacySettings();

        // eslint-disable-next-line guard-for-in
        for (let key in fmconfig) {
            let value = fmconfig[key];

            if (typeof value === 'string') {
                value = parse(fmconfig[key]);

                if (value === undefined) {
                    value = fmconfig[key];
                }
            }

            // @todo remove in 4 months
            if (key.startsWith('confirmModal_')) {
                mega.config.remove(key);
                continue;
            }

            mega.config.set(key, value);
        }

        refresh();
    };

    /**
     * Fetch server-side config.
     * @return {Promise}
     */
    ns.fetch = async function _fetchConfig() {
        if (!u_handle) {
            throw new Error('Unable to fetch fmconfig in the current context.');
        }
        setup(await Promise.resolve(mega.attr.get(u_handle, 'fmconfig', false, true)).catch(nop));

        // disable client-side rubbish scheduler
        if (u_attr.flags.ssrs > 0) {
            mega.config.remove('rubsched');
        }

        // Initialize account notifications.
        mega.notif.setup(fmconfig.anf);
    };

    /**
     * Flush any pending fmconfig storage
     * @returns {MegaPromise}
     */
    ns.flush = async function() {
        if (timer) {
            delay.cancel('fmconfig:store');
            return timer instanceof Promise ? timer : store();
        }
    };

    /**
     * Retrieve configuration value.
     * (We'll keep using the global `fmconfig` for now)
     *
     * @param {String} key Configuration key
     */
    ns.get = function _getConfigValue(key) {
        return fmconfig[key];
    };

    /**
     * Remove configuration value
     * @param {String} key   Configuration key
     */
    ns.remove = function _removeConfigValue(key) {
        return this.set(key, undefined);
    };

    /**
     * Store configuration value
     * @param {String} key   Configuration key
     * @param      {*} value Configuration value
     */
    ns.set = function _setConfigValue(key, value) {
        fmconfig[key] = value;
    };

    /**
     * Same as .set, but displays a toast notification.
     * @param {String} key          Configuration key
     * @param      {*} value        Configuration value
     * @param {String} [toastText]  Toast notification text
     */
    ns.setn = function _setConfigValueToast(key, value, toastText) {

        delay('fmconfig:setn.' + key, function() {
            let toast = false;

            if (key === 'rubsched' && u_attr.flags.ssrs > 0) {
                value = String(value).split(':').pop() | 0;

                if (M.account.ssrs !== value) {
                    M.account.ssrs = value;
                    mega.attr.set('rubbishtime', String(value), -2, 1);
                    toast = true;
                }
            }
            else if (mega.config.get(key) !== value) {
                mega.config.set(key, value);
                toast = true;
            }

            if (toast) {
                showToast('settings', toastText || l[16168]);
            }
        });
    };

    /**
     * Factory to store boolean-type options as bits to save space.
     * @param {String} name The name for the fmconfig property
     * @param {Array} properties Array of options/preferences
     * @returns {{}}
     */
    ns.factory = function(name, properties) {
        assert(Array.isArray(properties) && properties.length < 32);
        assert(typeof name === 'string' && name.length > 1 && name.length < 9);

        const config = Object.create(null);
        const bitdef = Object.create(null);

        let flags = mega.config.get(name) >>> 0;
        for (let i = properties.length; i--;) {
            const k = properties[i];
            const v = 1 << i;

            if (flags & v) {
                config[k] = true;
            }
            bitdef[k] = v >>> 0;
        }

        const define = (target, prop, value) => {
            let rc = false;

            if (prop in bitdef) {
                const old = flags;

                if (value | 0) {
                    flags |= bitdef[prop];
                    rc = Reflect.set(target, prop, true);
                }
                else {
                    flags &= ~bitdef[prop];
                    rc = Reflect.deleteProperty(target, prop);
                }

                if (rc && old !== flags) {
                    mega.config.set(name, flags || undefined);
                }
            }
            return rc;
        };

        return new Proxy(config, {
            get(target, prop) {
                return prop in bitdef ? !!(flags & bitdef[prop]) : undefined;
            },
            set(target, prop, value) {
                return define(target, prop, value);
            },
            deleteProperty(target, prop) {
                return define(target, prop, null);
            }
        });
    };

    mBroadcaster.once('startMega', function() {
        setup(parse(sessionStorage.fmconfig || localStorage.fmconfig));
    });

    if (is_karma) {
        mega.config = ns;
    }
    else {
        Object.defineProperty(mega, 'config', {value: Object.freeze(ns)});
    }
})();

// --------------------------------------------------------------------------
// --------------------------------------------------------------------------
// --- Account Notifications (preferences) ----------------------------------
// --------------------------------------------------------------------------
(function(map) {
    'use strict';

    let _enum = [];
    const _tag = 'ACCNOTIF_';

    Object.keys(map)
        .forEach(function(k) {
            map[k] = map[k].map(function(m) {
                return k.toUpperCase() + '_' + m.toUpperCase();
            });

            let rsv = 0;
            let memb = clone(map[k]);

            while (memb.length < 10) {
                memb.push(k.toUpperCase() + '_RSV' + (++rsv));
            }

            if (memb.length > 10) {
                throw new Error('Stack overflow..');
            }

            _enum = _enum.concat(memb);
        });

    makeEnum(_enum, _tag, mega);

    Object.defineProperty(mega, 'notif', {
        value: Object.freeze((function(flags) {
            function check(flag, tag) {
                if (typeof flag === 'string') {
                    if (tag !== undefined) {
                        flag = tag + '_' + flag;
                    }
                    flag = String(flag).toUpperCase();
                    flag = mega[flag] || mega[_tag + flag] || 0;
                }
                return flag;
            }

            return {
                get flags() {
                    return flags;
                },

                setup: function setup(oldFlags) {
                    if (oldFlags === undefined) {
                        // Initialize account notifications to defaults (all enabled)
                        assert(!fmconfig.anf, 'Account notification flags already set');

                        Object.keys(map)
                            .forEach(k => {
                                const grp = map[k];
                                let len = grp.length;

                                while (len--) {
                                    this.set(grp[len]);
                                }
                            });
                    }
                    else {
                        flags = oldFlags;
                    }
                },

                has: function has(flag, tag) {
                    return flags & check(flag, tag);
                },

                set: function set(flag, tag) {
                    flags |= check(flag, tag);
                    mega.config.set('anf', flags);
                },

                unset: function unset(flag, tag) {
                    flags &= ~check(flag, tag);
                    mega.config.set('anf', flags);
                }
            };
        })(0))
    });

    _enum = undefined;

})({
    chat: ['ENABLED'],
    cloud: ['ENABLED', 'NEWSHARE', 'DELSHARE', 'NEWFILES'],
    contacts: ['ENABLED', 'FCRIN', 'FCRACPT', 'FCRDEL']
});

var xxtea = (function() {
    'use strict';

    // (from https://github.com/xxtea/xxtea-js/blob/master/src/xxtea.js)
    var DELTA = 0x9E3779B9;
    var ns = Object.create(null);

    var int32 = function(i) {
        return i & 0xFFFFFFFF;
    };

    var mx = function(sum, y, z, p, e, k) {
        return (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
    };

    ns.encryptUint32Array = function encryptUint32Array(v, k) {
        var length = v.length;
        var n = length - 1;
        var y;
        var z = v[n];
        var sum = 0;
        var e;
        var p;
        var q;
        for (q = Math.floor(6 + 52 / length) | 0; q > 0; --q) {
            sum = int32(sum + DELTA);
            e = sum >>> 2 & 3;
            for (p = 0; p < n; ++p) {
                y = v[p + 1];
                z = v[p] = int32(v[p] + mx(sum, y, z, p, e, k));
            }
            y = v[0];
            z = v[n] = int32(v[n] + mx(sum, y, z, n, e, k));
        }
        return v;
    };

    ns.decryptUint32Array = function decryptUint32Array(v, k) {
        var length = v.length;
        var n = length - 1;
        var y = v[0];
        var z;
        var sum;
        var e;
        var p;
        var q = Math.floor(6 + 52 / length);
        for (sum = int32(q * DELTA); sum !== 0; sum = int32(sum - DELTA)) {
            e = sum >>> 2 & 3;
            for (p = n; p > 0; --p) {
                z = v[p - 1];
                y = v[p] = int32(v[p] - mx(sum, y, z, p, e, k));
            }
            z = v[n];
            y = v[0] = int32(v[0] - mx(sum, y, z, 0, e, k));
        }
        return v;
    };

    return Object.freeze(ns);
}());

var use_ssl = window.is_extension && !window.is_iframed ? 0 : 1;
var have_ab = typeof ArrayBuffer !== 'undefined' && typeof DataView !== 'undefined';

// general errors
var EINTERNAL = -1;
var EARGS = -2;
var EAGAIN = -3;
var ERATELIMIT = -4;
var EFAILED = -5;
var ETOOMANY = -6;
var ERANGE = -7;
var EEXPIRED = -8;

// FS access errors
var ENOENT = -9;            // No Entity (does not exist)
var ECIRCULAR = -10;
var EACCESS = -11;
var EEXIST = -12;
var EINCOMPLETE = -13;

// crypto errors
var EKEY = -14;

// user errors
var ESID = -15;
var EBLOCKED = -16;
var EOVERQUOTA = -17;
var ETEMPUNAVAIL = -18;
var ETOOMANYCONNECTIONS = -19;
var EGOINGOVERQUOTA = -24;

var EROLLEDBACK = -25;
var EMFAREQUIRED = -26;     // Multi-Factor Authentication Required
var EMASTERONLY = -27;
var EBUSINESSPASTDUE = -28;
var EPAYWALL = -29;     // ODQ paywall state

// custom errors
var ETOOERR = -400;
var ESHAREROVERQUOTA = -401;


// convert user-supplied password array
function prepare_key(a) {
    var i, j, r;
    var aes = [];
    var pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56];

    for (j = 0; j < a.length; j += 4) {
        var key = [0, 0, 0, 0];
        for (i = 0; i < 4; i++) {
            if (i + j < a.length) {
                key[i] = a[i + j];
            }
        }
        aes.push(new sjcl.cipher.aes(key));
    }

    for (r = 65536; r--;) {
        for (j = 0; j < aes.length; j++) {
            pkey = aes[j].encrypt(pkey);
        }
    }

    return pkey;
}

// prepare_key with string input
function prepare_key_pw(password) {
    return prepare_key(str_to_a32(password));
}

function a32_to_base64(a) {
    return base64urlencode(a32_to_str(a));
}

var firefox_boost = is_chrome_firefox && !!localStorage.fxboost;

// ArrayBuffer to binary string
var ab_to_str = function abToStr1(ab) {
    return ab.buffer;
};

if (firefox_boost) {
    ab_to_str = mozAB2S;
}
else if (have_ab) {
    ab_to_str = function abToStr2(ab) {
        var u8 = new Uint8Array(ab);

        /*if (u8.length < 0x10000) {
         return String.fromCharCode.apply(String, u8);
         }*/

        var b = '';
        for (var i = 0; i < u8.length; i++) {
            b = b + String.fromCharCode(u8[i]);
        }

        return b;
    };
}

// random number between 0 .. n -- based on repeated calls to rc
function rand(n) {
    var r = new Uint32Array(1);
    asmCrypto.getRandomValues(r);
    return r[0] % n; // <- oops, it's uniformly distributed only when `n` divides 0x100000000
}


/**
 * generate RSA key
 * @param {Function} callBack   optional callback function to be called.
 *                              if not specified the standard set_RSA will be called
 */
var crypto_rsagenkey = promisify(function _crypto_rsagenkey(resolve, reject, aSetRSA) {
    'use strict';
    var logger = MegaLogger.getLogger('crypt');

    var startTime = new Date();

    // suppress upgrade warning at account creation time
    mega.keyMgr.postregistration = true;

    if (typeof msCrypto !== 'undefined' && msCrypto.subtle) {
        var ko = msCrypto.subtle.generateKey({
            name: 'RSAES-PKCS1-v1_5',
            modulusLength: 2048
        }, true);
        ko.oncomplete = function () {
            ko = msCrypto.subtle.exportKey('jwk', ko.result.privateKey);
            ko.oncomplete = function () {
                var jwk = JSON.parse(asmCrypto.bytes_to_string(new Uint8Array(ko.result)));
                _done(['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'].map(function (x) {
                    return base64urldecode(jwk[x]);
                }));
            };
        };
    }
    else {
        var w = new Worker((is_extension ? '' : '/') + 'keygen.js');

        w.onmessage = function (e) {
            w.terminate();
            _done(e.data);
        };

        var workerSeed = mega.getRandomValues(256);

        w.postMessage([2048, 257, workerSeed]);
    }

    function _done(k) {
        var endTime = new Date();
        logger.debug("Key generation took "
                     + (endTime.getTime() - startTime.getTime()) / 1000.0
            + " seconds!");

        if (aSetRSA === false) {
            resolve(k);
        }
        else {
            u_setrsa(k).then(resolve).catch(dump);
        }
    }
});

function ApiQueue() { // double storage
    'use strict';
    this._head = 0;
    this._tail = 0;
    this._storage1 = Object.create(null);
    this._storage2 = Object.create(null);
}
ApiQueue.prototype.size = function () {
    'use strict';
    return this._tail - this._head;
};
ApiQueue.prototype.sneak = function () {
    'use strict';
    if (this._head !== this._tail) {
        return { st1: this._storage1[this._head], st2: this._storage2[this._head] };
    }
};
ApiQueue.prototype.enqueue = function (data1, data2) {
    'use strict';
    // reset to 0 index, we dont want indexes to keep getting bigger
    // this is very safe, since it takes place during enqueue,
    // and it is not possible to have another undergoing dequeue(because head=tail)
    if (this._head === this._tail) {
        this._head = 0;
        this._tail = 0;
    }
    this._storage1[this._tail] = data1;
    this._storage2[this._tail++] = data2;
};
ApiQueue.prototype.clear = function () {
    'use strict';
    this._head = 0;
    this._tail = 0;
    this._storage1 = Object.create(null);
    this._storage2 = Object.create(null);
};
ApiQueue.prototype.dequeue = function (onlySingle) {
    'use strict';
    if (this._head !== this._tail) {
        var data1 = this._storage1[this._head];
        if (onlySingle && data1.length) {
            return null;
        }
        var data2 = this._storage2[this._head];
        delete this._storage1[this._head];
        delete this._storage2[this._head++];

        return { st1: data1, st2: data2 };
    }
};


/**
 * Converts a Unicode string to a UTF-8 cleanly encoded string.
 *
 * @param {String} unicode
 *     Browser's native string encoding.
 * @return {String}
 *     UTF-8 encoded string (8-bit characters only).
 */
var to8 = firefox_boost ? mozTo8 : function (unicode) {
    return unescape(encodeURIComponent(unicode));
};

// API command queueing
// All commands are executed in sequence, with no overlap
// FIXME: show user warning after backoff > 1000

var apixs = [];

function api_reset() {
    "use strict";

    // user account API interface
    api_init(0, 'cs');

    // folder link API interface
    api_init(1, 'cs');

    // active view's SC interface (chunked mode)
    api_init(2, 'sc', { '{[a{'      : sc_packet,     // SC command
                        '{[a{{t[f{' : sc_node,       // SC node
                        '{[a{{t[f2{': sc_node,       // SC node (versioned)
                        '{'         : sc_residue,    // SC residue
                        '#'         : api_esplit }); // numeric error code


    // user account event notifications
    api_init(3, 'sc');

    // active view's initial tree fetch (chunked mode)
    api_init(4, 'cs', { '[{[ok0{' : tree_ok0,        // tree shareownerkey
                        '[{[f{'   : tree_node,       // tree node
                        '[{[f2{'  : tree_node,       // tree node (versioned)
                        '['       : tree_residue,    // tree residue
                        '#'       : api_esplit });   // numeric error code

    // WSC interface (chunked mode)
    api_init(5, 'wsc', {
        '{[a{': sc_packet,     // SC command
        '{[a{{t[f{': sc_node,       // SC node
        '{[a{{t[f2{': sc_node,       // SC node (versioned)
        '{': sc_residue,    // SC residue
        '#': api_esplit // numeric error code
    });

    // off band attribute requests (keys) for chat
    api_init(6, 'cs');

}

mBroadcaster.once('boot_done', api_reset);

// a chunked request received a purely numerical response - handle it the usual way
function api_esplit(e) {
    api_reqerror(this.q, e, false);
}

function api_setsid(sid) {
    "use strict";

    if (sid !== false) {
        watchdog.notify('setsid', sid);

        if (typeof dlmanager === 'object') {

            delay('overquota:retry', () => dlmanager._onOverQuotaAttemptRetry(sid));
        }
        sid = 'sid=' + sid;
    }
    else {
        sid = '';
    }

    apixs[0].sid = sid;
    apixs[2].sid = sid;
    apixs[3].sid = sid;
    apixs[4].sid = sid;
    apixs[5].sid = sid;
    apixs[6].sid = sid;

    if (self.fetchStreamSupport && mega.requestStatusMonitor) {
        mega.requestStatusMonitor.init();
    }
}

function api_setfolder(h) {
    "use strict";

    h = 'n=' + h;

    if (u_sid) {
        h += '&sid=' + u_sid;
    }

    apixs[1].sid = h;
    apixs[2].sid = h;
    apixs[4].sid = h;
    apixs[5].sid = h;
}

function stopapi() {
    "use strict";

    if (typeof M === 'object' && $.len(M._apiReqInflight)) {
        if (d) {
            console.warn('Aborting in-flight API requests...', M._apiReqInflight);
        }

        M._apiReqInflight = Object.create(null);
        M._apiReqPollCache = Object.create(null);
    }

    for (var i = apixs.length; i--;) {
        api_cancel(apixs[i]);
        apixs[i].cmdsQueue.clear();
        apixs[i].cmdsBuffer = [];
        apixs[i].ctxsBuffer = [];
    }
}

function api_cancel(q) {
    "use strict";

    if (q) {
        if (q.xhr) {
            // setting the "cancelled" flag ensures that
            // subsequent onerror/onload/onprogress callbacks are ignored.
            q.xhr.cancelled = true;
            q.xhr.onprogress = q.xhr.onloadend = null;
            if (q.xhr.abort) q.xhr.abort();
            q.xhr = false;
        }
        if (q.timer) {
            clearTimeout(q.timer);
        }
    }
}

function api_init(channel, service, split) {
    "use strict";

    if (apixs[channel]) {
        api_cancel(apixs[channel]);
    }

    apixs[channel] = {
        c: channel,                 // channel
        cmdsQueue: new ApiQueue(),    // queued executing commands + contexts
        cmdsBuffer: [],               // pulled cmds from queue (under processing)
        ctxsBuffer: [],               // pulled ctxs from queue
        i: 0,                       // currently executing buffer
        seqno: -Math.floor(Math.random() * 0x100000000), // unique request start ID
        xhr: false,                 // channel XMLHttpRequest
        timer: false,               // timer for exponential backoff
        failhandler: api_reqfailed, // request-level error handler
        backoff: 0,                 // timer backoff
        service: service,           // base URI component
        sid: '',                    // sid URI component (optional)
        split: split,               // associated JSON splitter rules, presence triggers progressive/chunked mode
        splitter: false,            // JSONSplitter instance implementing .split
        rawreq: false,
        setimmediate: false
    };
}


/**
 * queue request on API channel
 * @param {Object} request              request object to be sent to API
 * @param {Object} context              context object to be returned with response, has 'callback' func to be called
 * @param {Number} channel              optional - channel number to use (default =0)
 */
function api_req(request, context, channel) {
    "use strict";

    if (channel === undefined) {
        channel = 0;
    }

    if (d) console.debug("API request on " + channel + ": " + JSON.stringify(request));

    if (context === undefined) {
        context = Object.create(null);
    }

    var q = apixs[channel];

    q.cmdsQueue.enqueue(request, context);

    if (!q.setimmediate) {
        q.setimmediate = setTimeout(api_proc, 0, q);
    }
}

// indicates whether this is a Firefox supporting the moz-chunked-*
// responseType or a Chrome derivative supporting the fetch API
// values: unknown: -1, no: 0, moz-chunked: 1, fetch: 2
// FIXME: check for fetch on !Firefox, not just on Chrome
var chunked_method = window.chrome ? (self.fetch ? 2 : 0) : -1;

if (typeof Uint8Array.prototype.indexOf !== 'function' || is_firefox_web_ext) {
    if (d) {
        console.debug('No chunked method on this browser: ' + ua);
    }
    chunked_method = 0;
}

// this kludge emulates moz-chunked-arraybuffer with XHR-style callbacks
async function chunkedfetch(xhr, uri, body) {
    'use strict';

    let signal;
    if (typeof AbortController !== 'undefined') {
        const controller = new AbortController();
        xhr.abort = async() => controller.abort();
        signal = controller.signal;
    }
    const evt = {loaded: 0};
    const highWaterMark = BACKPRESSURE_HIGHWATERMARK;
    const response = await fetch(uri, {method: body ? 'POST' : 'GET', body, signal});

    xhr.status = response.status;
    xhr.totalBytes = response.headers.get('Original-Content-Length') | 0;

    if (typeof WritableStream !== 'undefined' && mega.shouldApplyNetworkBackPressure(xhr.totalBytes)) {
        const queueingStrategy =
            typeof ByteLengthQueuingStrategy !== 'undefined'
            && 'highWaterMark' in ByteLengthQueuingStrategy.prototype
            && new ByteLengthQueuingStrategy({highWaterMark});

        return response.body.pipeTo(new WritableStream({
            async write(chunk) {
                // console.warn('fetch/write', chunk.byteLength);

                xhr.response = chunk;
                evt.loaded += chunk.byteLength;
                xhr.onprogress(evt);

                while (decWorkerPool.busy || fmdb && fmdb.busy) {
                    if (d) {
                        console.info('fetch/backpressure (%d%%)', evt.loaded * 100 / xhr.totalBytes);
                    }
                    // apply backpressure
                    await sleep(BACKPRESSURE_WAIT_TIME);
                }
            },
            close() {
                xhr.response = null;
                console.debug('fetch/close');
            },
            abort(ex) {
                xhr.response = null;
                console.error('fetch/abort', ex);
            }
        }, queueingStrategy));
    }

    const reader = response.body.getReader();
    while (true) {
        const {value, done} = await reader.read();

        if (done) {
            break;
        }

        // feed received chunk to JSONSplitter via .onprogress()
        xhr.response = value;
        evt.loaded += value.length;
        xhr.onprogress(evt);
    }

    xhr.response = null;
}

// send pending API request on channel q
function api_proc(q) {
    "use strict";

    if (q.setimmediate) {
        clearTimeout(q.setimmediate);
        q.setimmediate = false;
    }

    if (q.cmdsQueue.size() === 0 || q.cmdsBuffer.length && q.cmdsBuffer.length > 0) {
        return;
    }
    var currCmd = [];
    var currCtx = [];
    var element = q.cmdsQueue.dequeue(); // only first element alone
    if (element) {
        currCmd.push(element.st1);
        currCtx.push(element.st2);
        if (!element.st1.length) { // we will distinguish String + array of CMDs
            element = q.cmdsQueue.dequeue(true);
            while (element) {
                currCmd.push(element.st1);
                currCtx.push(element.st2);
                element = q.cmdsQueue.dequeue(true);
            }
        }
    }

    q.cmdsBuffer = currCmd;
    q.ctxsBuffer = currCtx;


    if (!q.xhr) {
        // we need a real XHR only if we don't use fetch for this channel
        q.xhr = (!q.split || chunked_method != 2) ? getxhr() : Object.create(null);

        q.xhr.q = q;

        // JSON splitters are keen on processing the data as soon as it arrives,
        // so route .onprogress to it.
        if (q.split) {
            q.xhr.onprogress = function(evt) {
                if (!this.cancelled) {
                    // caller wants progress updates? give caller progress updates!
                    if (this.q.ctxsBuffer[0] && this.q.ctxsBuffer[0].progress) {
                        var progressPercent = 0;
                        var bytes = evt.total || this.totalBytes;

                        if (!bytes) {
                            // this may throw an exception if the header doesn't exist
                            try {
                                bytes = this.getResponseHeader('Original-Content-Length') | 0;
                                this.totalBytes = bytes;
                            }
                            catch (e) {}
                        }

                        if (evt.loaded > 0 && bytes > 2) {
                            this.q.ctxsBuffer[0].progress(evt.loaded / bytes * 100);
                        }
                    }

                    // update number of bytes received in .onprogress() for subsequent check
                    // in .onloadend() whether it contains more data

                    var chunk = this.response;

                    // is this an ArrayBuffer? turn into a Uint8Array
                    if (!(chunk.length >= 0)) chunk = new Uint8Array(chunk);

                    if (!chunked_method) {
                        // unfortunately, we're receiving a growing response
                        this.q.received = chunk.length;
                    }
                    else {
                        // wonderful, we're receiving a chunked response
                        this.q.received += chunk.length;
                    }

                    // send incoming live data to splitter
                    // for maximum flexibility, the splitter ctx will be the XHR
                    if (!this.q.splitter.chunkproc(chunk, chunk.length === this.totalBytes)) {
                        // a JSON syntax error occurred: hard reload
                        fm_fullreload(this.q, 'onerror JSON Syntax Error');
                    }
                }
            };
        }

        q.xhr.onloadend = function onAPIProcXHRLoad(ev) {
            if (!this.cancelled) {
                var t;
                var status = this.status;

                if (status == 200) {
                    var response = this.response;

                    // is this residual data that hasn't gone to the splitter yet?
                    if (this.q.splitter) {
                        // we process the full response if additional bytes were received
                        // FIXME: in moz-chunked transfers, if onload() can contain chars beyond
                        // the last onprogress(), send .substr(this.q.received) instead!
                        // otherwise, we send false to indicate no further input
                        // in all cases, set the inputcomplete flag to catch incomplete API responses
                        if (!this.q.splitter.chunkproc((response && (response.length > this.q.received || typeof this.totalBytes === 'undefined')) ? response : false, true)) {
                            fm_fullreload(this.q, 'onload JSON Syntax Error');
                        }
                        return;
                    }

                    if (d) {
                        console.debug('API response:', response);
                    }

                    try {
                        t = JSON.parse(response);
                        if (response[0] == '{') {
                            t = [t];
                        }

                        status = true;
                    } catch (e) {
                        // bogus response, try again
                        if (d) {
                            console.debug(`Bad JSON data in response: ${response}`);
                        }
                        t = EAGAIN;
                    }
                }
                else {
                    if (d) {
                        console.debug(`API server connection failed (error ${status})`);
                    }
                    t = ERATELIMIT;
                }

                if (typeof t === 'object') {
                    var ctxs = this.q.ctxsBuffer;
                    var paywall;
                    for (var i = 0; i < ctxs.length; i++) {
                        var ctx = ctxs[i];

                        if (typeof ctx.callback === 'function') {
                            var res = t[i];

                            if (res && res.err < 0) {
                                // eslint-disable-next-line max-depth
                                if (d) {
                                    console.debug('APIv2 Custom Error Detail', res, this.q.cmdsBuffer[i]);
                                }
                                ctx.v2APIError = res;
                                res = res.err;
                            }
                            if (res === EPAYWALL) {
                                paywall = true;
                            }
                            ctx.callback(res, ctx, this, t);
                        }
                    }
                    if (paywall) {
                        api_reqerror(this.q, EPAYWALL, status);
                    }

                    // reset state for next request
                    api_ready(this.q);
                    api_proc(this.q);
                }
                else {
                    if (ev && ev.type === 'error') {
                        if (d) {
                            console.debug("API request error - retrying");
                        }
                    }
                    api_reqerror(this.q, t, status);
                }
            }
        };
    }

    if (q.rawreq === false) {
        q.url = apipath + q.service
              + '?id=' + (q.seqno++)
              + '&' + q.sid
              + (q.split ? '&ec' : '')  // encoding: chunked if splitter attached
              + mega.urlParams();       // additional parameters

        if (typeof q.cmdsBuffer[0] === 'string') {
            q.url += '&' + q.cmdsBuffer[0];
            delete q.rawreq;
        }
        else {
            if (q.cmdsBuffer.length === 1 && q.cmdsBuffer[0].length) {
                q.url += '&bc=1';
                q.rawreq = JSON.stringify(q.cmdsBuffer[0]);
            }
            else {
                q.rawreq = JSON.stringify(q.cmdsBuffer);
            }
        }
    }

    api_send(q);
}

function api_send(q) {
    "use strict";

    q.timer = false;

    if (q.xhr === false) {
        if (d) {
            console.debug(`API request aborted: ${q.rawreq} to ${q.url}`);
        }
        return;
    }

    if (d) {
        console.debug(
            'Sending API request: %s', q.rawreq || q.cmdsBuffer[0],
            String(q.url).replace(/sid=[\w-]+/, 'sid=\u2026'));
    }

    // reset number of bytes received and response size
    q.received = 0;
    delete q.xhr.totalBytes;

    q.xhr.cancelled = false;

    if (q.split && chunked_method == 2) {
        // use chunked fetch with JSONSplitter input type Uint8Array
        q.splitter = new JSONSplitter(q.split, q.xhr, true);

        chunkedfetch(q.xhr, q.url, q.rawreq)
            .then(() => q.xhr && q.xhr.onloadend())
            .catch(ex => {
                if (d) {
                    console.error('Fetch error.', ex);
                }

                if (q.xhr) {
                    // at this point fake a partial data to trigger a retry..
                    q.xhr.status = 206;
                    q.xhr.onloadend();
                }
            });
    }
    else {
        // use legacy XHR API
        q.xhr.open('POST', q.url, true);

        if (q.split) {
            if (chunked_method) {
                // FIXME: use Fetch API if more efficient than this
                q.xhr.responseType = 'moz-chunked-arraybuffer';

                // first try? record success
                if (chunked_method < 0) {
                    chunked_method = q.xhr.responseType == 'moz-chunked-arraybuffer';
                }
            }

            // at this point, chunked_method being logically true implies arraybuffer
            q.splitter = new JSONSplitter(q.split, q.xhr, chunked_method);
         }

        q.xhr.send(q.rawreq);
    }
}

function api_reqerror(q, e, status) {
    'use strict';
    var c = e | 0;

    if (typeof e === 'object' && e.err < 0) {
        c = e.err | 0;
    }

    if (c === EAGAIN || c === ERATELIMIT) {
        // request failed - retry with exponential backoff
        if (q.backoff) {
            q.backoff = Math.min(600000, q.backoff << 1);
        }
        else {
            q.backoff = 125+Math.floor(Math.random()*600);
        }

        q.timer = setTimeout(api_send, q.backoff, q);

        c = EAGAIN;
    }
    else {
        q.failhandler(q.c, e);
    }

    if (mega.state & window.MEGAFLAG_LOADINGCLOUD) {
        if (status === true && c === EAGAIN) {
            mega.loadReport.EAGAINs++;
        }
        else if (status === 500) {
            mega.loadReport.e500s++;
        }
        else {
            mega.loadReport.errs++;
        }
    }
}

function api_ready(q) {
    q.rawreq = false;
    q.backoff = 0; // request succeeded - reset backoff timer
    q.cmdsBuffer = [];
    q.ctxsBuffer = [];
}

var apiFloorCap = 3000;

function api_retry() {
    "use strict";

    for (var i = apixs.length; i--; ) {
        if (apixs[i].timer && apixs[i].backoff > apiFloorCap) {
            clearTimeout(apixs[i].timer);
            apixs[i].backoff = apiFloorCap;
            apixs[i].timer = setTimeout(api_send, apiFloorCap, apixs[i]);
        }
    }
}

function api_reqfailed(channel, error) {
    'use strict';

    var e = error | 0;
    var c = channel | 0;
    if (d) {
        console.error('API req failed. Channel=' + c + '  Error: ', e);
    }
    if (typeof error === 'object' && error.err < 0) {
        e = error.err | 0;
    }

    // does this failure belong to a folder link, but not on the SC channel?
    if (apixs[c].sid[0] === 'n' && c !== 2) {
        // yes: handle as a failed folder link access
        return folderreqerr(c, error);
    }

    if (e === ESID) {
        u_logout(true);
        Soon(function() {
            showToast('clipboard', l[19]);
        });
        loadingInitDialog.hide('force');
        if (page !== 'download') {
            loadSubPage('login');
        }
    }
    else if ((c === 2 || c === 5) && e === ETOOMANY) {
        // too many pending SC requests - reload from scratch
        fm_fullreload(this.q, 'ETOOMANY');
    }
    // if suspended account
    else if (e === EBLOCKED) {
        var queue = apixs[c];
        queue.rawreq = false;
        queue.cmdsQueue.clear();
        queue.cmdsBuffer = [];
        queue.ctxsBuffer = [];
        queue.setimmediate = false;

        api_req({ a: 'whyamiblocked' }, {
            callback: function whyAmIBlocked(reasonCode) {
                var setLogOutOnNavigation = function() {
                    onIdle(function() {
                        mBroadcaster.once('pagechange', function() {
                            u_logout().then(() => location.reload(true));
                        });
                    });
                    window.doUnloadLogOut = 0x9001;
                    return false;
                };

                // On clicking OK, log the user out and redirect to contact page
                loadingDialog.hide();

                var reasonText = '';
                var dialogTitle = l[17768];// Terminated account

                if (reasonCode === 200) {
                    dialogTitle = l[6789];// Suspended account
                    reasonText = l[17741];// Your account has been suspended due to multiple breaches of Mega's Terms..
                }
                else if (reasonCode === 300) {
                    reasonText = l[17740];// Your account was terminated due to breach of Mega's Terms of Service...
                }
                else if (reasonCode === 400) {
                    reasonText = l[19748];// Your account is disabled by administrator
                }
                else if (reasonCode === 401) {
                    reasonText = l[20816];// Your account is deleted (business user)
                }
                else if (reasonCode === 500) {

                    // Handle SMS verification for suspended account
                    if (is_mobile) {
                        loadSubPage('sms/add-phone-suspended');
                    }
                    else {
                        sms.phoneInput.init(true);
                    }

                    // Allow user to escape from SMS verification dialog in order to login a different account.
                    return setLogOutOnNavigation();
                }
                else if (reasonCode === 700) {
                    var to = String(page).startsWith('emailverify') && 'login-to-account';
                    security.showVerifyEmailDialog(to);

                    // Allow user to escape from Email verification dialog in order to login a different account.
                    return setLogOutOnNavigation();
                }
                else {
                    // Unknown reasonCode
                    reasonText = l[17740]; // Your account was terminated due to breach of Mega's Terms of Service...
                }

                // Log the user out for all scenarios except SMS required (500)
                u_logout(true);

                // if fm-overlay click handler was initialized, we remove the handler to prevent dialog skip
                $('.fm-dialog-overlay').off('click.fm');
                if (is_mobile) {
                    parsepage(pages['mobile']);
                }
                msgDialog('warninga', dialogTitle,
                    reasonText,
                    false,
                    function () {
                        var redirectUrl = getAppBaseUrl() + '#contact';
                        window.location.replace(redirectUrl);
                    }
                );
            }
        });
    }
    else if (e === EPAYWALL) {
        if (window.M) {
            if (M.account && u_attr && !u_attr.uspw) {
                M.account = null;
            }
            if (window.loadingDialog) {
                loadingDialog.hide();
            }
            M.showOverStorageQuota(e);
        }
    }
    else {
        api_reqerror(apixs[c], EAGAIN, 0);
    }
}

var failxhr;
var failtime = 0;

function api_reportfailure(hostname, callback) {
    if (!hostname) {
        return Soon(callback);
    }

    var t = new Date().getTime();

    if (t - failtime < 60000) {
        return;
    }
    failtime = t;

    if (failxhr) {
        failxhr.abort();
    }

    failxhr = getxhr();
    failxhr.open('POST', apipath + 'pf?h', true);
    failxhr.callback = callback;

    failxhr.onload = function () {
        if (this.status === 200) {
            failxhr.callback();
        }
    };

    failxhr.send(hostname);
}

var waiturl;
var waitxhr;
var waitbackoff = 125;
var waittimeout;
var waitbegin;
var waitid = 0;
var cmsNotifHandler = localStorage.cmsNotificationID || 'Nc4AFJZK';

function stopsc() {
    "use strict";

    if (waitxhr && waitxhr.readyState !== waitxhr.DONE) {
        waitxhr.abort();
        waitxhr = false;
    }

    if (waittimeout) {
        delay.cancel(waittimeout);
        waittimeout = false;
    }
}

// if set, further sn updates are disallowed (the local state has become invalid)
function setsn(sn) {
    "use strict";

    // update sn in DB, triggering a "commit" of the current "transaction"
    if (fmdb) {
        attribCache.flush();
        fmdb.add('_sn', { i : 1, d : sn });
    }
}

// are we processing historical SC commands?
var initialscfetch;

// last step of the streamed SC response processing
function sc_residue(sc) {
    "use strict";

    if (d) {
        console.info('sc-residue', [sc], initialscfetch, scqtail, scqhead);
    }

    if (sc.sn) {
        const didLoadFromAPI = mega.loadReport.mode === 2;

        // enqueue new sn
        if (initialscfetch || currsn !== sc.sn || scqhead !== scqtail) {
            currsn = sc.sn;
            scq[scqhead++] = [{a: '_sn', sn: currsn}];
            resumesc();
        }

        if (initialscfetch) {
            // we have concluded the post-load SC fetch, as we have now
            // run out of new actionpackets: show filemanager!
            scq[scqhead++] = [{ a: '_fm' }];
            initialscfetch = false;
            resumesc();

            if (didLoadFromAPI && !pfid) {

                mega.keyMgr.pendingpullkey = true;
            }
        }

        // we're done, wait for more
        if (sc.w) {
            waiturl = sc.w + '?' + apixs[5].sid + '&sn=' + currsn;
        }
        else if (!waiturl) {
            console.error("Strange error, we dont know WSC url and we didnt get it");
            waitbackoff = Math.min(9e3, waitbackoff << 1);
            waittimeout = setTimeout(getsc, waitbackoff, true);
            return;
        }
        waittimeout = delay('reinit:wsc', waitsc, waitbackoff);

        if ((mega.state & window.MEGAFLAG_LOADINGCLOUD) && !mega.loadReport.recvAPs) {
            mega.loadReport.recvAPs = Date.now() - mega.loadReport.stepTimeStamp;
            mega.loadReport.stepTimeStamp = Date.now();
        }
    }
    else {
        // malformed SC response - take the conservative route and reload fully
        // FIXME: add one single retry if !sscount: Clear scq, clear worker state,
        // then reissue getsc() (difficult to get right - be cautious)
        return fm_fullreload(null, 'malformed SC response');
    }

    // (mandatory steps at the conclusion of a successful split response)
    if (this.q) {
        api_ready(this.q);
        api_proc(this.q);
    }
}

// getsc() serialisation (getsc() can be called anytime from anywhere if
// someone thinks that it is beneficial!)
var gettingsc;

// request new actionpackets and stream them to sc_packet() as they come in
// nodes in t packets are streamed to sc_node()
function getsc(force) {
    "use strict";

    if (force) {
        gettingsc = true;

        if (waitxhr) {
            waitxhr.abort();
        }

        api_cancel(apixs[5]);   // retire existing XHR that may still be completing the request
        if (currsn) {
            api_ready(apixs[5]);
            api_req('sn=' + currsn, {}, 5);

            if (window.loadingInitDialog.progress) {
                window.loadingInitDialog.step3(loadfm.fromapi ? 40 : 1, 55);
            }

            if (mega.state & window.MEGAFLAG_LOADINGCLOUD) {
                mega.loadReport.scSent = Date.now();
            }
        }
        else {
            if (d) {
                console.error('Get WSC is called but without SN, it\'s a bug... please trace');
            }
            eventlog(99737);
        }
    }
}

function waitsc() {
    "use strict";

    var MAX_WAIT = 40e3;
    var newid = ++waitid;

    stopsc();

    if (!waitxhr) {
        waitxhr = getxhr();
    }
    waitxhr.waitid = newid;

    waittimeout = delay('reinit:wsc', waitsc, MAX_WAIT);

    waitxhr.onloadend = function(ev) {
        if (this.waitid === waitid) {
            if (this.status !== 200) {
                if (d) {
                    console.info('waitsc(%s:%s)', this.status, ev.type, ev);
                }

                waitbackoff = Math.min(MAX_WAIT, waitbackoff << 1);
                waittimeout = delay('reinit:wsc', waitsc, waitbackoff);
            }
            else {
                // Increase backoff if we do keep receiving packets is rapid succession, so that we maintain
                // smaller number of connections to process more data at once - backoff up to 4 seconds.
                stopsc();
                if (Date.now() - waitbegin < 1000) {
                    waitbackoff = Math.min(4e3, waitbackoff << 1);
                }
                else {
                    waitbackoff = 250;
                }

                var delieveredResponse = this.response;
                if (delieveredResponse === '0') {
                    // clearTimeout(waittimeout); mo need for clearing, we stopped
                    // immediately re-connect.
                    waittimeout = delay('reinit:wsc', waitsc, 1);
                }
                else if ($.isNumeric(delieveredResponse)) {
                    if (delieveredResponse == ETOOMANY) {
                        // WSC is stopped at the beginning.
                        fm_fullreload(null, 'ETOOMANY');
                    }
                    else if (delieveredResponse == EAGAIN || delieveredResponse == ERATELIMIT) {
                        // WSC is stopped at the beginning.
                        waittimeout = delay('reinit:wsc', waitsc, waitbackoff);
                    }
                    else if (delieveredResponse == EBLOCKED) {
                        // == because API response will be in a string
                        api_reqfailed(5, EBLOCKED);
                    }
                }
                else if (!apixs[5].split) {
                    console.error('WSC has no splitter !!!!');
                }
                else {
                    var wscSplitter = new JSONSplitter(apixs[5].split, waitxhr, false);
                    if (!wscSplitter.chunkproc(delieveredResponse, true)) {
                        fm_fullreload(null, 'onload JSON Syntax Error');
                    }
                }
            }
        }
    };

    waitxhr.onprogress = function() {
        waittimeout = delay('reinit:wsc', waitsc, MAX_WAIT);
    };

    waitbegin = Date.now();
    waitxhr.open('POST', waiturl, true);
    waitxhr.send();
}
mBroadcaster.once('startMega', function() {
    'use strict';

    window.addEventListener('online', function(ev) {
        if (d) {
            console.info(ev);
        }
        api_retry();

        if (waiturl) {
            waitsc();
        }
    });

    var invisibleTime;
    document.addEventListener('visibilitychange', function(ev) {

        if (document.hidden) {
            invisibleTime = Date.now();
        }
        else {
            invisibleTime = Date.now() - invisibleTime;

            if (mega.loadReport && !mega.loadReport.sent) {
                if (!mega.loadReport.invisibleTime) {
                    mega.loadReport.invisibleTime = 0;
                }
                mega.loadReport.invisibleTime += invisibleTime;
            }
        }

        mBroadcaster.sendMessage('visibilitychange:' + Boolean(document.hidden));
    });
});

function api_create_u_k() {
    'use strict';

    // static master key, will be stored at the server side encrypted with the master pw
    u_k = [...crypto.getRandomValues(new Uint32Array(4))];
}

// If the user triggers an action that requires an account, but hasn't logged in,
// we create an anonymous preliminary account. Returns userhandle and passwordkey for permanent storage.
function api_createuser(ctx, invitecode, invitename, uh) {
    var logger = MegaLogger.getLogger('crypt');
    var i;
    var ssc = Array(4); // session self challenge, will be used to verify password
    var req, res;

    if (!ctx.passwordkey) {
        ctx.passwordkey = Array(4);
        for (i = 4; i--;) {
            ctx.passwordkey[i] = rand(0x100000000);
        }
    }

    if (!u_k) {
        api_create_u_k();
    }

    for (i = 4; i--;) {
        ssc[i] = rand(0x100000000);
    }

    logger.debug("api_createuser - masterkey: " + u_k + " passwordkey: " + ctx.passwordkey);

    // in business sub-users API team decided to hack "UP" command to include "UC2" new arguments.
    // so now. we will check if this is a business sub-user --> we will add extra arguments to "UP" (crv,hak,v)

    var doApiRequest = function (request) {
        if (mega.affid) {
            req.aff = mega.affid;
        }
        logger.debug("Storing key: " + request.k);

        api_req(request, ctx);
        watchdog.notify('createuser');
    };

    req = {
            a: 'up',
            k: a32_to_base64(encrypt_key(new sjcl.cipher.aes(ctx.passwordkey), u_k)),
            ts: base64urlencode(a32_to_str(ssc) + a32_to_str(encrypt_key(new sjcl.cipher.aes(u_k), ssc)))
        };

    // invite code usage is obsolete. it's only used in case of business sub-users
    // therefore, if it exists --> we are registering a business sub-user
    if (invitecode) {
        req.ic = invitecode;
        req.name = invitename;

        security.deriveKeysFromPassword(ctx.businessUser, u_k,
            function (clientRandomValueBytes, encryptedMasterKeyArray32,
                hashedAuthenticationKeyBytes, derivedAuthenticationKeyBytes) {
                req.crv = ab_to_base64(clientRandomValueBytes);
                req.hak = ab_to_base64(hashedAuthenticationKeyBytes);
                req.v = 2;
                req.k = a32_to_base64(encryptedMasterKeyArray32);
                ctx.uh = ab_to_base64(derivedAuthenticationKeyBytes);

                doApiRequest(req);
            }
        );

    }
    else {
        doApiRequest(req);
    }

}

function api_checkconfirmcode(ctx, c) {
    res = api_req({
        a: 'uc',
        c: c
    }, ctx);
}

/* jshint -W098 */  // It is used in another file
function api_resetuser(ctx, emailCode, email, password) {

    // start fresh account
    api_create_u_k();

    var pw_aes = new sjcl.cipher.aes(prepare_key_pw(password));

    var ssc = Array(4);
    for (var i = 4; i--;) {
        ssc[i] = rand(0x100000000);
    }

    api_req({
        a: 'erx',
        c: emailCode,
        x: a32_to_base64(encrypt_key(pw_aes, u_k)),
        y: stringhash(email.toLowerCase(), pw_aes),
        z: base64urlencode(a32_to_str(ssc) + a32_to_str(encrypt_key(new sjcl.cipher.aes(u_k), ssc)))
    }, ctx);
}

function api_resetkeykey(ctx, code, key, email, pw) {

    'use strict';

    ctx.c = code;
    ctx.email = email;
    ctx.k = key;
    ctx.pw = pw;
    ctx.callback = api_resetkeykey2;

    api_req({
        a: 'erx',
        r: 'gk',
        c: code
    }, ctx);
}
/* jshint +W098 */

function api_resetkeykey2(res, ctx) {
    try {
        api_resetkeykey3(res, ctx);
    }
    catch (ex) {
        ctx.result(EKEY);
    }
}

function api_resetkeykey3(res, ctx) {

    'use strict';

    if (typeof res === 'string') {

        if (!verifyPrivateRsaKeyDecryption(res, ctx.k)) {
            ctx.result(EKEY);
        }
        else if (ctx.email && ctx.pw) {
            var pw_aes = new sjcl.cipher.aes(prepare_key_pw(ctx.pw));

            ctx.callback = ctx.result;
            api_req({
                a: 'erx',
                r: 'sk',
                c: ctx.c,
                x: a32_to_base64(encrypt_key(pw_aes, ctx.k)),
                y: stringhash(ctx.email.toLowerCase(), pw_aes)
            }, ctx);
        }
        else {
            ctx.result(0);
        }
    }
    else {
        ctx.result(res);
    }
}

/**
 * Verify that the Private RSA key was decrypted successfully by the Master/Recovery Key
 * @param {String} encryptedPrivateRsaKeyBase64 The encrypted Private RSA key as a Base64 string
 * @param {Array} masterKeyArray32 The Master/Recovery Key
 * @returns {Boolean} Returns true if the decryption succeeded, false if it failed
 */
function verifyPrivateRsaKeyDecryption(encryptedPrivateRsaKeyBase64, masterKeyArray32) {

    'use strict';

    try {
        // Decrypt the RSA key
        var privateRsaKeyArray32 = base64_to_a32(encryptedPrivateRsaKeyBase64);
        var cipherObject = new sjcl.cipher.aes(masterKeyArray32);
        var decryptedPrivateRsaKey = decrypt_key(cipherObject, privateRsaKeyArray32);
        var privateRsaKeyStr = a32_to_str(decryptedPrivateRsaKey);

        // Verify the integrity of the decrypted private key
        for (var i = 0; i < 4; i++) {
            var l = ((privateRsaKeyStr.charCodeAt(0) * 256 + privateRsaKeyStr.charCodeAt(1) + 7) >> 3) + 2;

            if (privateRsaKeyStr.substr(0, l).length < 2) {
                break;
            }
            privateRsaKeyStr = privateRsaKeyStr.substr(l);
        }

        // If invalid
        if (i !== 4 || privateRsaKeyStr.length >= 16) {
            return false;
        }

        return true;
    }
    catch (exception) {
        return false;
    }
}

// We query the sid using the supplied user handle (or entered email address, if already attached)
// and check the supplied password key.
// Returns [decrypted master key,verified session ID(,RSA private key)] or false if API error or
// supplied information incorrect
function api_getsid(ctx, user, passwordkey, hash, pinCode) {
    "use strict";

    ctx.callback = api_getsid2;
    ctx.passwordkey = passwordkey;

    // If previously blocked for too many login attempts, return early and show warning with time they can try again
    if (api_getsid.etoomany + 3600000 > Date.now() || location.host === 'webcache.googleusercontent.com') {
        return ctx.checkloginresult(ctx, ETOOMANY);
    }

    // Setup the login request
    var requestVars = { a: 'us', user: user, uh: hash };

    // If the two-factor authentication code was entered by the user, add it to the request as well
    if (pinCode !== null) {
        requestVars.mfa = pinCode;
    }

    api_req(requestVars, ctx);
}

api_getsid.warning = function() {
    var time = new Date(api_getsid.etoomany + 3780000).toLocaleTimeString();

    msgDialog('warningb', l[882], l[8855].replace('%1', time));
};

function api_getsid2(res, ctx) {
    var t, k;
    var r = false;

    // If the result is an error, pass that back to get an exact error
    if (typeof res === 'number') {
        ctx.checkloginresult(ctx, res);
        return false;
    }
    else if (typeof res === 'object') {
        var aes = new sjcl.cipher.aes(ctx.passwordkey);

        // decrypt master key
        if (typeof res.k === 'string') {
            k = base64_to_a32(res.k);

            if (k.length === 4) {
                k = decrypt_key(aes, k);

                aes = new sjcl.cipher.aes(k);

                if (typeof res.tsid === 'string') {
                    t = base64urldecode(res.tsid);
                    if (a32_to_str(encrypt_key(aes,
                            str_to_a32(t.substr(0, 16)))) === t.substr(-16)) {
                        r = [k, res.tsid];
                    }
                }
                else if (typeof res.u !== 'string' || res.u.length !== 11) {

                    console.error("Incorrect user handle in the 'us' response", res.u);

                    Soon(() => {
                        msgDialog('warninga', l[135], l[8853], res.u);
                    });

                    return false;
                }
                else if (typeof res.csid === 'string') {
                    let privk = null;
                    const errobj = {};
                    const t = base64urldecode(res.csid);

                    try {
                        privk = crypto_decodeprivkey(a32_to_str(decrypt_key(aes, base64_to_a32(res.privk))), errobj);
                    }
                    catch (ex) {

                        console.error('Error decoding private RSA key! %o', errobj, ex);

                        Soon(() => {
                            msgDialog('warninga', l[135], l[8853], JSON.stringify(errobj));
                        });

                        return false;
                    }

                    if (!privk) {
                        // Bad decryption of RSA is an indication that the password was wrong
                        console.error('RSA key decoding failed (%o)', errobj);
                        ctx.checkloginresult(ctx, false);
                        return false;
                    }

                    // Decrypt the Session ID
                    var decryptedSessionId = crypto_rsadecrypt(t, privk);

                    // Get the user handle from the decrypted Session ID (11 bytes starting at offset 16 bytes)
                    var sessionIdUserHandle = decryptedSessionId.substring(16, 27);

                    // Check that the decrypted sid and res.u aren't shorter than usual before making the comparison.
                    // Otherwise, we could construct an oracle based on shortened csids with single-byte user handles.
                    if (decryptedSessionId.length !== 255) {

                        console.error("Incorrect length of Session ID", decryptedSessionId.length, sessionIdUserHandle);

                        Soon(() => {
                            msgDialog('warninga', l[135], l[8853], decryptedSessionId.length);
                        });

                        return false;
                    }

                    // Check the user handle included in the Session ID matches the one sent in the 'us' response
                    if (sessionIdUserHandle !== res.u) {

                        console.error(
                            "User handle in Session ID did not match user handle from the 'us' request",
                            res.u, sessionIdUserHandle
                        );

                        Soon(() => {
                            msgDialog('warninga', l[135], l[8853], `${res.u} / ${sessionIdUserHandle}`);
                        });

                        return false;
                    }

                    // TODO: check remaining padding for added early wrong password detection likelihood
                    r = [k, base64urlencode(decryptedSessionId.substr(0, 43)), privk];
                }
            }
        }
    }

    // emailchange namespace exists, that means the user
    // attempted to verify their new email address without a session
    // therefore we showed them the login dialog. Now we call `emailchange.verify`
    // so the email verification can continue as expected.
    if (r && typeof emailchange === 'object') {
        emailchange.verify(new sjcl.cipher.aes(ctx.passwordkey), { k1: res.k, k2: k });
    }

    ctx.result(ctx, r);
}

// We call ug using the sid from setsid() and the user's master password to obtain the master key (and other credentials)
// Returns user credentials (.k being the decrypted master key) or false in case of an error.
function api_getuser(ctx) {
    api_req({
        a: 'ug'
    }, ctx);
}

/**
 * Send current node attributes to the API
 * @param {Object} n Updated node
 * @param {String} idtag mRandomToken
 * @return {MegaPromise}
 */
function api_setattr(n, idtag) {
    "use strict";

    var promise = new MegaPromise();
    var logger = MegaLogger.getLogger('crypt');

    var ctx = {
        callback: function(res) {
            if (res !== 0) {
                logger.error('api_setattr', res);
                promise.reject(res);
            }
            else {
                promise.resolve(res);
            }
        }
    };

    if (!crypto_keyok(n)) {
        logger.warn('Unable to set node attributes, invalid key on %s', n.h, n);
        return MegaPromise.reject(EKEY);
    }

    try {
        var at = ab_to_base64(crypto_makeattr(n));
        const ops = {a: 'a', n: n.h, at: at, i: idtag};

        if (M.getNodeRoot(n.h) === M.InboxID) {
            mega.backupCenter.ackVaultWriteAccess(n.h, ops);
        }

        logger.debug('Setting node attributes for "%s"...', n.h, idtag);

        api_req(ops, ctx);

        if (idtag) {
            M.scAckQueue[idtag] = Date.now();
        }
    }
    catch (ex) {
        logger.error(ex);
        promise.reject(ex);
    }

    return promise;
}

function stringhash(s, aes) {
    var s32 = str_to_a32(s);
    var h32 = [0, 0, 0, 0];
    var i;

    for (i = 0; i < s32.length; i++) {
        h32[i & 3] ^= s32[i];
    }

    for (i = 16384; i--;) {
        h32 = aes.encrypt(h32);
    }

    return a32_to_base64([h32[0], h32[2]]);
}

// Update user
// Can also be used to set keys and to confirm accounts (.c)
function api_updateuser(ctx, newuser) {
    newuser.a = 'up';

    if (mega.affid) {
        newuser.aff = mega.affid;
    }

    api_req(newuser, ctx);
}

var u_pubkeys = Object.create(null);

/**
 * Query missing keys for the given users.
 *
 * @return {MegaPromise}
 */
function api_cachepubkeys(users) {

    var logger = MegaLogger.getLogger('crypt');
    var u = [];
    var i;

    for (i = users.length; i--;) {
        if (users[i] !== 'EXP' && !u_pubkeys[users[i]]) {
            u.push(users[i]);
        }
    }

    // Fire off the requests and track them.
    var keyPromises = [];
    for (i = u.length; i--;) {
        keyPromises.push(crypt.getPubRSA(u[i]));
    }

    var gotPubRSAForEveryone = function() {
        for (i = u.length; i--;) {
            if (!u_pubkeys[u[i]]) {
                return false;
            }
        }
        return true;
    };
    var promise = new MegaPromise();

    // Make a promise for the bunch of them, and define settlement handlers.
    MegaPromise.allDone(keyPromises)
        .always(function __getKeysDone() {
            if (gotPubRSAForEveryone()) {
                logger.debug('Cached RSA pub keys for users ' + JSON.stringify(u));
                promise.resolve.apply(promise, arguments);
            }
            else {
                logger.warn('Failed to cache RSA pub keys for users' + JSON.stringify(u), arguments);
                promise.reject.apply(promise, arguments);
            }
        });

    return promise;
}

/**
 * Encrypts a cleartext data string to a contact.
 *
 * @param {String} user
 *     User handle of the contact.
 * @param {String} data
 *     Clear text to encrypt.
 * @return {String|Boolean}
 *     Encrypted cipher text, or `false` in case of unavailability of the RSA
 *     public key (needs to be obtained/cached beforehand).
 */
function encryptto(user, data) {
    'use strict';
    var pubkey;

    if ((pubkey = u_pubkeys[user])) {
        return crypto_rsaencrypt(data, pubkey, -0x4D454741);
    }

    return false;
}

/**
 * Add/cancel share(s) to a set of users or email addresses
 * targets is an array of {u,r} - if no r given, cancel share
 * If no sharekey known, tentatively generates one and encrypts
 * everything to it. In case of a mismatch, the API call returns
 * an error, and the whole operation gets repeated (exceedingly
 * rare race condition).
 *
 * @param {String} node
 *     Selected node id.
 * @param {Array} targets
 *     List of user email or user handle and access permission.
 * @param {Array} sharenodes
 *     Holds complete directory tree starting from given node.
 * @returns {MegaPromise}
 */
function api_setshare(node, targets, sharenodes) {

    var masterPromise  = new MegaPromise();

    // cache all targets' public keys
    var targetsPubKeys = [];

    for (var i = targets.length; i--;) {
        targetsPubKeys.push(targets[i].u);
    }

    var cachePromise = api_cachepubkeys(targetsPubKeys);
    cachePromise.done(function _cacheDone() {
        var setSharePromise = api_setshare1({ node: node, targets: targets, sharenodes: sharenodes });
        masterPromise.linkDoneAndFailTo(setSharePromise);
    });
    masterPromise.linkFailTo(cachePromise);

    return masterPromise;
}

/**
 * Actually enacts the setting/cancelling of shares.
 *
 * @param {Object} ctx
 *     Context for API commands.
 * @param {Array} params
 *     Additional parameters.
 * @returns {MegaPromise}
 */
function api_setshare1(ctx, params) {
    var logger = MegaLogger.getLogger('crypt');
    var i, j, n, nk, sharekey, ssharekey;
    var req, res;
    var newkey = true;
    var masterPromise = new MegaPromise();

    req = {
        a: 's2',
        n: ctx.node,
        s: ctx.targets,
        i: requesti
    };

    if (params) {
        logger.debug('api_setshare1.extend', params);
        for (i in params) {
            req[i] = params[i];
        }
    }

    const users = new Set();
    for (i = ctx.targets.length; i--;) {
        let {u} = ctx.targets[i];

        if (u && u !== 'EXP') {
            if (M.opc[u]) {
                console.error('Check this, got an outgoing pending contact...', u, M.opc[u]);

                u = M.opc[u].m;
                if (typeof u !== 'string' || !u.includes('@')) {
                    console.assert(false, 'Invalid outgoing pending contact email...');
                    continue;
                }
            }
            users.add(u);
        }
    }

    for (i = req.s.length; i--;) {
        if (typeof req.s[i].r !== 'undefined') {

            if (mega.keyMgr.secure) {
                newkey = mega.keyMgr.hasNewShareKey(ctx.node);

                // dummy key/handleauth - FIXME: remove
                req.ok = a32_to_base64([0, 0, 0, 0]);
                req.ha = a32_to_base64([0, 0, 0, 0]);
                break;
            }

            if (!req.ok) {
                if (u_sharekeys[ctx.node]) {
                    sharekey = u_sharekeys[ctx.node][0];
                    newkey = mega.keyMgr.hasNewShareKey(ctx.node);
                }
                else {
                    // we only need to generate a key if one or more shares are being added to a previously unshared node
                    sharekey = [];
                    for (j = 4; j--;) {
                        sharekey.push(rand(0x100000000));
                    }
                    crypto_setsharekey(ctx.node, sharekey, true);
                }

                req.ok = a32_to_base64(encrypt_key(u_k_aes, sharekey));
                req.ha = crypto_handleauth(ctx.node);
                ssharekey = a32_to_str(sharekey);
            }
        }
    }

    if (newkey) {
        req.cr = crypto_makecr(ctx.sharenodes, [ctx.node], true);
    }

    ctx.backoff = 97;
    ctx.maxretry = 4;
    ctx.ssharekey = ssharekey;

    // encrypt ssharekey to known users
    for (i = req.s.length; i--;) {
        if (!mega.keyMgr.secure && u_pubkeys[req.s[i].u]) {
            req.s[i].k = base64urlencode(crypto_rsaencrypt(ssharekey, u_pubkeys[req.s[i].u]));
        }
        if (typeof req.s[i].m !== 'undefined') {
            req.s[i].u = req.s[i].m;
        }

        if (M.opc[req.s[i].u]) {
            if (d) {
                logger.warn(req.s[i].u + ' is an outgoing pending contact, fixing to email...', M.opc[req.s[i].u].m);
            }
            // the caller incorrectly passed a handle for a pending contact, so fixup..
            req.s[i].u = M.opc[req.s[i].u].m;
        }
    }

    ctx.req = req;

    /** Callback for API interactions. */
    ctx.callback = function (res, ctx) {
        if (typeof res === 'object') {

            mega.keyMgr.setUsedNewShareKey(ctx.node).catch(dump);
            masterPromise.resolve(res);

            /* sharekey clashes will be resolved via ^!keys
            if (res.ok) {
                logger.debug('Share key clash: Set returned key and try again.');
                ctx.req.ok = res.ok;
                var k = decrypt_key(u_k_aes, base64_to_a32(res.ok));
                crypto_setsharekey(ctx.node, k);
                ctx.req.ha = crypto_handleauth(ctx.node);

                var ssharekey = a32_to_str(k);

                for (var i = ctx.req.s.length; i--;) {
                    if (u_pubkeys[ctx.req.s[i].u]) {
                        ctx.req.s[i].k = base64urlencode(crypto_rsaencrypt(ssharekey,
                            u_pubkeys[ctx.req.s[i].u]));
                    }
                }
                logger.info('Retrying share operation.');
                api_req(ctx.req, ctx);
            }
            else {
                logger.info('Share succeeded.');
                masterPromise.resolve(res);
            }
*/
        }
        else if (!--ctx.maxretry || res === EARGS) {
            logger.error('Share operation failed.', res);
            masterPromise.reject(res);
        }
        else {
            logger.info('Retrying share operation...');

            setTimeout(function() {
                api_req(ctx.req, ctx);
            }, ctx.backoff <<= 1);
        }
    };

    logger.info('Invoking share operation.');

    mega.keyMgr.sendShareKeys(ctx.node, [...users])
        .then(() => {
            api_req(ctx.req, ctx);
        })
        .catch((ex) => {
            logger.error(ex);
            masterPromise.reject(ex);
        });

    return masterPromise;
}

function crypto_handleauth(h) {
    return a32_to_base64(encrypt_key(u_k_aes, str_to_a32(h + h)));
}

function crypto_keyok(n) {
    "use strict";

    return n && typeof n.k === 'object' && n.k.length >= (n.t ? 4 : 8);
}

function crypto_encodepubkey(pubkey) {
    var mlen = pubkey[0].length * 8,
        elen = pubkey[1].length * 8;

    return String.fromCharCode(mlen / 256) + String.fromCharCode(mlen % 256) + pubkey[0]
        + String.fromCharCode(elen / 256) + String.fromCharCode(elen % 256) + pubkey[1];
}

function crypto_decodepubkey(pubk) {
    var pubkey = [];

    var keylen = pubk.charCodeAt(0) * 256 + pubk.charCodeAt(1);

    // decompose public key
    for (var i = 0; i < 2; i++) {
        if (pubk.length < 2) {
            break;
        }

        var l = (pubk.charCodeAt(0) * 256 + pubk.charCodeAt(1) + 7) >> 3;
        if (l > pubk.length - 2) {
            break;
        }

        pubkey[i] = pubk.substr(2, l);
        pubk = pubk.substr(l + 2);
    }

    // check format
    if (i !== 2 || pubk.length >= 16) {
        return false;
    }

    pubkey[2] = keylen;

    return pubkey;
}

function crypto_encodeprivkey(privk) {
    var plen = privk[3].length * 8,
        qlen = privk[4].length * 8,
        dlen = privk[2].length * 8,
        ulen = privk[7].length * 8;

    var t = String.fromCharCode(qlen / 256) + String.fromCharCode(qlen % 256) + privk[4]
        + String.fromCharCode(plen / 256) + String.fromCharCode(plen % 256) + privk[3]
        + String.fromCharCode(dlen / 256) + String.fromCharCode(dlen % 256) + privk[2]
        + String.fromCharCode(ulen / 256) + String.fromCharCode(ulen % 256) + privk[7];

    while (t.length & 15) t += String.fromCharCode(rand(256));

    return t;
}

function crypto_encodeprivkey2(privk) {
    'use strict';
    const plen = privk[3].length * 8;
    const qlen = privk[4].length * 8;
    const dlen = privk[2].length * 8;

    return String.fromCharCode(qlen / 256) + String.fromCharCode(qlen % 256) + privk[4]
        + String.fromCharCode(plen / 256) + String.fromCharCode(plen % 256) + privk[3]
        + String.fromCharCode(dlen / 256) + String.fromCharCode(dlen % 256) + privk[2];
}

/**
 * Decode private RSA key.
 * @param {String} privk the key to decode.
 * @param {Object} [errobj] Optional object to put the details of a failure, if any
 * @returns {Array|Boolean} decoded private key, or boolean(false) if failure.
 */
function crypto_decodeprivkey(privk, errobj) {
    'use strict';
    let i, l;
    let privkey = [];

    // decompose private key
    for (i = 0; i < 4; i++) {
        if (privk.length < 2) {
            break;
        }

        l = (privk.charCodeAt(0) * 256 + privk.charCodeAt(1) + 7) >> 3;
        if (l > privk.length - 2) {
            break;
        }

        privkey[i] = new asmCrypto.BigNumber(privk.substr(2, l));
        privk = privk.substr(l + 2);
    }

    // check format
    if (i !== 4 || privk.length >= 16) {
        return false;
    }

    // TODO: check remaining padding for added early wrong password detection likelihood

    // restore privkey components via the known ones
    const q = privkey[0];
    const p = privkey[1];
    const d = privkey[2];
    const u = privkey[3];
    const q1 = q.subtract(1);
    const p1 = p.subtract(1);
    const m = new asmCrypto.Modulus(p.multiply(q));
    const e = new asmCrypto.Modulus(p1.multiply(q1)).inverse(d);
    const dp = d.divide(p1).remainder;
    const dq = d.divide(q1).remainder;

    // Calculate inverse modulo of q under p
    const inv = new asmCrypto.Modulus(p).inverse(q);

    // Convert Uint32Arrays to hex for comparison
    const hexInv = asmCrypto.bytes_to_hex(inv.toBytes()).replace(/^0+/, '');
    const hexU = asmCrypto.bytes_to_hex(u.toBytes()).replace(/^0+/, '');

    // Detect private key blob corruption - prevent API-exploitable RSA oracle requiring 500+ logins.
    // Ensure the bit length being at least 1000 and that u is indeed the inverse modulo of q under p.
    if (!(p.bitLength > 1000 &&
        q.bitLength > 1000 &&
        d.bitLength > 2000 &&
        u.bitLength > 1000 &&
        hexU === hexInv)) {
        return false;
    }

    privkey = [m, e, d, p, q, dp, dq, u];
    for (i = 0; i < privkey.length; i++) {
        privkey[i] = asmCrypto.bytes_to_string(privkey[i].toBytes());
    }

    return privkey;
}

/**
 * Decode private RSA key (pqd format).
 * @param {Uint8Array} privk the key to decode.
 * @param {Object} [errobj] Optional object to put the details of a failure, if any
 * @returns {Array|Boolean} decoded private key, or boolean(false) if failure.
 */
function crypto_decodeprivkey2(privk) {
    'use strict';
    let i, l;
    let pos = 0;
    let privkey = [];

    // decompose private key
    for (i = 0; i < 3; i++) {
        if (pos + 2 > privk.length) {
            return false;
        }

        l = privk[pos] * 256 + (privk[pos + 1] + 7) >> 3;
        pos += 2;

        if (pos + l > privk.length) {
            return false;
        }

        privkey[i] = new asmCrypto.BigNumber(privk.slice(pos, pos + l));
        pos += l;
    }

    // restore privkey components via the known ones
    const q = privkey[0];
    const p = privkey[1];
    const d = privkey[2];
    const q1 = q.subtract(1);
    const p1 = p.subtract(1);
    const m = new asmCrypto.Modulus(p.multiply(q));
    const e = new asmCrypto.Modulus(p1.multiply(q1)).inverse(d);
    const dp = d.divide(p1).remainder;
    const dq = d.divide(q1).remainder;

    // Calculate inverse modulo of q under p
    const u = new asmCrypto.Modulus(p).inverse(q);

    privkey = [m, e, d, p, q, dp, dq, u];
    for (i = 0; i < privkey.length; i++) {
        privkey[i] = asmCrypto.bytes_to_string(privkey[i].toBytes());
    }

    return privkey;
}


/**
 * Encrypts a cleartext string with the supplied public key.
 *
 * @param {String} cleartext
 *     Clear text to encrypt.
 * @param {Array} pubkey
 *     Public encryption key (in the usual internal format used).
 * @return {String}
 *     Encrypted cipher text.
 */
function crypto_rsaencrypt(cleartext, pubkey, bf) {
    'use strict';

    if (bf !== -0x4d454741 && mega.keyMgr.secure) {
        return '';
    }

    // random padding up to pubkey's byte length minus 2
    for (var i = (pubkey[0].length) - 2 - cleartext.length; i-- > 0;) {
        cleartext += String.fromCharCode(rand(256));
    }

    var ciphertext = asmCrypto.bytes_to_string(asmCrypto.RSA_RAW.encrypt(cleartext, pubkey));

    var clen = ciphertext.length * 8;
    ciphertext = String.fromCharCode(clen / 256) + String.fromCharCode(clen % 256) + ciphertext;

    return ciphertext;
}

var storedattr = Object.create(null);
var faxhrs = Object.create(null);
var faxhrfail = Object.create(null);
var faxhrlastgood = Object.create(null);

// data.byteLength & 15 must be 0
function api_storefileattr(id, type, key, data, ctx, ph) {
    var handle = typeof ctx === 'string' && ctx;

    if (typeof ctx !== 'object') {
        if (!storedattr[id]) {
            storedattr[id] = Object.create(null);
        }

        if (key) {
            data = asmCrypto.AES_CBC.encrypt(data, a32_to_ab(key), false);
        }

        ctx = {
            id: id,
            ph: ph,
            type: type,
            data: data,
            handle: handle,
            callback: api_fareq,
            startTime: Date.now()
        };
    }

    var req = {
        a: 'ufa',
        s: ctx.data.byteLength,
        ssl: use_ssl
    };

    if (M.d[ctx.handle] && M.getNodeRights(ctx.handle) > 1) {
        req.h = handle;
    }
    else if (ctx.ph) {
        req.ph = ctx.ph;
    }

    api_req(req, ctx, pfid ? 1 : 0);
}

async function api_getfileattr(fa, type, procfa, errfa) {
    'use strict';
    let r;
    const p = Object.create(null);
    const h = Object.create(null);
    const k = Object.create(null);
    const plain = Object.create(null);
    let cache = nop;

    type |= 0;
    if (type in fa_handler.lru) {
        const lru = await fa_handler.lru[type];
        if (!lru.error) {
            const send = async(h, ab) => procfa({cached: 1}, h, ab);
            const found = await lru.bulkGet(Object.keys(fa)).catch(dump) || false;

            for (const h in found) {
                fa[h] = null;
                send(h, found[h]).catch(dump);
            }
            cache = (h, buf) => lru.set(h, buf).catch(dump);
        }
    }

    const re = new RegExp(`(\\d+):${type}\\*([\\w-]+)`);
    for (const n in fa) {
        if (fa[n] && (r = re.exec(fa[n].fa))) {
            const t = base64urldecode(r[2]);
            if (t.length === 8) {
                if (!h[t]) {
                    h[t] = n;
                    k[t] = fa[n].k;
                }

                if (!p[r[1]]) {
                    p[r[1]] = t;
                }
                else {
                    p[r[1]] += t;
                }
                plain[r[1]] = !!fa[n].plaintext;
            }
        }
        else if (fa[n] !== null && typeof errfa === 'function') {
            queueMicrotask(errfa.bind(null, n));
        }
    }

    // eslint-disable-next-line guard-for-in
    for (const n in p) {
        const ctx = {
            callback: api_fareq,
            type: type,
            p: p[n],
            h: h,
            k: k,
            procfa: (ctx, h, buf) => {
                if (!buf || !buf.byteLength) {
                    buf = 0xDEAD;
                }
                else {
                    cache(h, buf);
                }
                return procfa(ctx, h, buf);
            },
            errfa: errfa,
            startTime: Date.now(),
            plaintext: plain[n]
        };
        api_req({
            a: 'ufa',
            fah: base64urlencode(ctx.p.substr(0, 8)),
            ssl: use_ssl,
            r: +fa_handler.chunked
        }, ctx);
    }
}

// @todo refactor whole fa-handler from scratch!
lazy(fa_handler, 'lru', () => {
    'use strict';
    const lru = Object.create(null);
    lazy(lru, 0, () => LRUMegaDexie.create('fa-handler.0', 1e4));
    lazy(lru, 1, () => LRUMegaDexie.create('fa-handler.1', 1e3));
    return lru;
});

function fa_handler(xhr, ctx) {
    var logger = d > 1 && MegaLogger.getLogger('crypt');
    var chunked = ctx.p && fa_handler.chunked;

    this.xhr = xhr;
    this.ctx = ctx;
    this.pos = 0;

    if (chunked) {
        if (!fa_handler.browser) {
            fa_handler.browser = browserdetails(ua).browser;
        }

        if (ctx.plaintext) {
            this.setParser('arraybuffer', this.plain_parser)
        }
        else {
            switch (fa_handler.browser) {
            case 'Firefox':
                this.parse = this.moz_parser;
                this.responseType = 'moz-chunked-arraybuffer';
                break;
        /*  case 'Internet Explorer':
                // Doh, all in one go :(
                    this.parse = this.stream_parser;
                    this.responseType = 'ms-stream';
                    this.stream_reader= this.msstream_reader;
                    break;*/
        /*  case 'Chrome':
                this.parse = this.stream_parser;
                this.responseType = 'stream';
                break;*/
            default:
                this.setParser('text');
            }
        }

        this.done = this.Finish;
    }
    else {
        this.responseType = 'arraybuffer';
        if (ctx.p) {
            this.proc = this.GetFA;
        }
        else {
            this.proc = this.PutFA;
        }
        this.done = this.onDone;
    }

    if (logger) {
        logger.debug('fah type:', this.responseType);
    }
}
fa_handler.chunked = true;
fa_handler.abort = function () {
    var logger = MegaLogger.getLogger('crypt');
    for (var i = 0; faxhrs[i]; i++) {
        if (faxhrs[i].readyState && faxhrs[i].readyState !== 4 && faxhrs[i].ctx.p) {
            var ctx = faxhrs[i].ctx;
            faxhrs[i].ctx = {
                fabort: 1
            };
            faxhrs[i].fah.parse = null;

            logger.debug('fah_abort', i, faxhrs[i]);

            faxhrs[i].abort();

            for (var i in ctx.h) {
                ctx.procfa(ctx, ctx.h[i], 0xDEAD);
            }
        }
    }
};
fa_handler.prototype = {
    PutFA: function (response) {
        var logger = MegaLogger.getLogger('crypt');
        var ctx = this.ctx;

        logger.debug("Attribute storage successful for faid=" + ctx.id + ", type=" + ctx.type);

        if (!storedattr[ctx.id]) {
            storedattr[ctx.id] = Object.create(null);
        }

        storedattr[ctx.id][ctx.type] = ab_to_base64(response);

        if (storedattr[ctx.id].target) {
            logger.debug("Attaching to existing file");
            api_attachfileattr(storedattr[ctx.id].target, ctx.id);
        }
    },

    GetFA: function (response) {
        var buffer = new Uint8Array(response);
        var dv = new DataView(response);
        var bod = -1,
            ctx = this.ctx;
        var h, j, p, l, k;

        i = 0;
        const procfa = (res) => ctx.procfa(ctx, ctx.h[h], res);
        const decrypt = tryCatch((k) => {
            const ts = new Uint8Array(response, p, l);

            const data = asmCrypto.AES_CBC.decrypt(ts, a32_to_ab([
                k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]
            ]), false);

            procfa(data);
        }, () => procfa(0xDEAD));

        // response is an ArrayBuffer structured
        // [handle.8 position.4] data
        do {
            p = dv.getUint32(i + 8, true);
            if (bod < 0) {
                bod = p;
            }

            if (i >= bod - 12) {
                l = response.byteLength - p;
            }
            else {
                l = dv.getUint32(i + 20, true) - p;
            }

            h = '';

            for (j = 0; j < 8; j++) {
                h += String.fromCharCode(buffer[i + j]);
            }
            if (!ctx.h[h]) {
                break;
            }

            if ((k = ctx.k[h])) {
                decrypt(k);
            }

            i += 12;
        } while (i < bod);
    },

    setParser: function (type, parser) {
        var logger = MegaLogger.getLogger('crypt');
        if (type) {
            if (type === 'text' && !parser) {
                this.parse = this.str_parser;
            }
            else {
                this.parse = parser.bind(this);
            }
            this.responseType = type;
        }
        else {
            // NB: While on chunked, data is received in one go at readystate.4
            this.parse = this.ab_parser;
            this.responseType = 'arraybuffer';
        }
        if (this.xhr.readyState === 1) {
            this.xhr.responseType = this.responseType;
            logger.debug('New fah type:', this.xhr.responseType);
        }
    },

    plain_parser: function (data) {
        if (this.xhr.readyState === 4) {
            if (!this.xpos) {
                this.xpos = 12;
            }
            var bytes = data.slice(this.xpos)
            if (bytes.byteLength > 0) {
                this.ctx.procfa(this.ctx, this.ctx.k[this.ctx.p], bytes);
                this.xpos += bytes.byteLength
            }
        }
    },

    str_parser: function (data) {
        if (this.xhr.readyState > 2) {
            this.pos += this.ab_parser(str_to_ab(data.slice(this.pos))) | 0;
        }
    },

    msstream_reader: function (stream) {
        var logger = MegaLogger.getLogger('crypt');
        var self = this;
        var reader = new MSStreamReader();
        reader.onload = function (ev) {
            logger.debug('MSStream result', ev.target);

            self.moz_parser(ev.target.result);
            self.stream_parser(0x9ff);
        };
        reader.onerror = function (e) {
            logger.error('MSStream error', e);
            self.stream_parser(0x9ff);
        };
        reader.readAsArrayBuffer(stream);
    },

    stream_reader: function (stream) {
        var logger = MegaLogger.getLogger('crypt');
        var self = this;
        stream.readType = 'arraybuffer';
        stream.read().then(function (result) {
                logger.debug('Stream result', result);

                self.moz_parser(result.data);
                self.stream_parser(0x9ff);
            },
            function (e) {
                logger.error('Stream error', e);
                self.stream_parser(0x9ff);
            });
    },

    stream_parser: function (stream, ev) {
        var logger = MegaLogger.getLogger('crypt');
        // www.w3.org/TR/streams-api/
        // https://code.google.com/p/chromium/issues/detail?id=240603

        logger.debug('Stream Parser', stream);

        if (stream === 0x9ff) {
            if (this.wstream) {
                if (this.wstream.length) {
                    this.stream_reader(this.wstream.shift());
                }
                if (!this.wstream.length) {
                    delete this.wstream;
                }
            }
        }
        else if (this.wstream) {
            this.wstream.push(stream);
        }
        else {
            this.wstream = [];
            this.stream_reader(stream);
        }
    },

    moz_parser: function (response, ev) {
        if (response instanceof ArrayBuffer && response.byteLength > 0) {
            response = new Uint8Array(response);
            if (this.chunk) {
                var tmp = new Uint8Array(this.chunk.byteLength + response.byteLength);
                tmp.set(this.chunk)
                tmp.set(response, this.chunk.byteLength);
                this.chunk = tmp;
            }
            else {
                this.chunk = response;
            }

            var offset = this.ab_parser(this.chunk.buffer);
            if (offset) {
                this.chunk = this.chunk.subarray(offset);
            }
        }
    },

    ab_parser: function (response, ev) {
        var logger = d > 1 && MegaLogger.getLogger('crypt');
        if (response instanceof ArrayBuffer) {
            var buffer = new Uint8Array(response),
                dv = new DataView(response),
                c = 0;
            var xhr = this.xhr,
                ctx = this.ctx,
                i = 0,
                p, h, k, l = buffer.byteLength;

            while (i + 12 < l) {
                p = dv.getUint32(i + 8, true);
                if (i + 12 + p > l) {
                    break;
                }
                h = String.fromCharCode.apply(String, buffer.subarray(i, i + 8));
                // logger.debug(ctx.h[h], i, p, !!ctx.k[h]);

                i += 12;
                if (ctx.h[h] && (k = ctx.k[h])) {
                    var td;
                    var ts = buffer.subarray(i, p + i);

                    try {
                        k = a32_to_ab([k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]);
                        td = asmCrypto.AES_CBC.decrypt(ts, k, false);
                        ++c;
                    }
                    catch (ex) {
                        console.warn(ex);
                        td = 0xDEAD;
                    }
                    ctx.procfa(ctx, ctx.h[h], td);
                }
                i += p;
            }

            if (logger) {
                logger.debug('ab_parser.r', i, p, !!h, c);
            }

            return i;
        }
    },

    onDone: function (ev) {
        var logger = MegaLogger.getLogger('crypt');
        var ctx = this.ctx,
            xhr = this.xhr;

        if (xhr.status === 200 && typeof xhr.response === 'object') {
            if (!xhr.response || xhr.response.byteLength === 0) {
                logger.warn('api_fareq: got empty response...', xhr.response);
                xhr.faeot();
            }
            else {
                this.proc(xhr.response);
                faxhrlastgood[xhr.fa_host] = Date.now();
            }
        }
        else {
            if (ctx.p) {
                logger.debug("File attribute retrieval failed (" + xhr.status + ")");
                xhr.faeot();
            }
            else {
                api_faretry(ctx, xhr.status, xhr.fa_host);
            }
        }

        this.Finish();
    },

    Finish: function () {
        var pending = this.chunk && this.chunk.byteLength
            || (this.pos && this.xhr.response.substr(this.pos).length);

        if (pending) {
            if (!fa_handler.errors) {
                fa_handler.errors = 0;
            }

            if (++fa_handler.errors === 7) {
                fa_handler.chunked = false;
            }

            console.warn(this.xhr.fa_host + ' connection interrupted (chunked fa)');
        }

        oDestroy(this);

        return pending;
    }
};

function api_faretry(ctx, error, host) {
    var logger = MegaLogger.getLogger('crypt');
    if (ctx.faRetryI) {
        ctx.faRetryI *= 1.8;
    }
    else {
        ctx.faRetryI = 250;
    }

    if (!ctx.p && error === EACCESS) {
        api_pfaerror(ctx.handle);
    }

    if (ctx.errfa && ctx.errfa.timeout && ctx.faRetryI > ctx.errfa.timeout) {
        api_faerrlauncher(ctx, host);
    }
    else if (error !== EACCESS && ctx.faRetryI < 5e5) {
        logger.debug("Attribute " + (ctx.p ? 'retrieval' : 'storage') + " failed (" + error + "), retrying...",
                     ctx.faRetryI);

        return setTimeout(function () {
            ctx.startTime = Date.now();
            if (ctx.p) {
                api_req({
                    a: 'ufa',
                    fah: base64urlencode(ctx.p.substr(0, 8)),
                    ssl: use_ssl,
                    r: +fa_handler.chunked
                }, ctx);
            }
            else {
                api_storefileattr(null, null, null, null, ctx);
            }

        }, ctx.faRetryI);
    }

    mBroadcaster.sendMessage('fa:error', ctx.id, error, ctx.p, 2);
    console.warn("File attribute " + (ctx.p ? 'retrieval' : 'storage') + " failed (" + error + " @ " + host + ")");
}

function api_faerrlauncher(ctx, host) {
    var logger = MegaLogger.getLogger('crypt');
    var r = false;
    var id = ctx.p && ctx.h[ctx.p] && preqs[ctx.h[ctx.p]] && ctx.h[ctx.p];

    if (d) {
        logger.error('FAEOT', id);
    }

    if (id !== slideshow_handle()) {
        if (id) {
            pfails[id] = 1;
            delete preqs[id];
        }
    }
    else {
        r = true;
        ctx.errfa(id, 1);
    }
    return r;
}

function api_fareq(res, ctx, xhr) {
    var logger = d > 1 && MegaLogger.getLogger('crypt');
    var error = typeof res === 'number' && res || '';

    if (ctx.startTime && logger) {
        logger.debug('Reply in %dms for %s', (Date.now() - ctx.startTime), xhr.q.url);
    }

    if (error) {
        api_faretry(ctx, error, hostname(xhr.q && xhr.q.url));
    }
    else if (typeof res === 'object' && res.p) {
        var data;
        var slot, i, t;
        var p, pp = [res.p],
            m;

        for (i = 0; p = res['p' + i]; i++) {
            pp.push(p);
        }

        for (m = pp.length; m--;) {
            for (slot = 0;; slot++) {
                if (!faxhrs[slot]) {
                    faxhrs[slot] = getxhr();
                    break;
                }

                if (faxhrs[slot].readyState === XMLHttpRequest.DONE) {
                    break;
                }
            }

            faxhrs[slot].ctx = ctx;
            faxhrs[slot].fa_slot = slot;
            faxhrs[slot].fa_timeout = ctx.errfa && ctx.errfa.timeout;
            faxhrs[slot].fah = new fa_handler(faxhrs[slot], ctx);

            if (logger) {
                logger.debug("Using file attribute channel " + slot);
            }

            faxhrs[slot].onprogress = function (ev) {
                    if (logger) {
                    logger.debug('fah ' + ev.type, this.readyState, ev.loaded, ev.total,
                            typeof this.response === 'string'
                                ? this.response.substr(0, 12).split("").map(function (n) {
                                        return (n.charCodeAt(0) & 0xff).toString(16)
                                    }).join(".")
                                : this.response, ev);
                    }
                    if (this.fa_timeout) {
                        if (this.fart) {
                            clearTimeout(this.fart);
                        }
                        var xhr = this;
                        this.fart = setTimeout(function() {
                            xhr.faeot();
                            xhr = undefined;
                        }, this.fa_timeout);
                    }

                    if (this.fah.parse && this.response) {
                        this.fah.parse(this.response, ev);
                    }
                };

            faxhrs[slot].faeot = function () {
                    if (faxhrs[this.fa_slot]) {
                        faxhrs[this.fa_slot] = undefined;
                        this.fa_slot = -1;

                        if (this.ctx.errfa) {
                            if (api_faerrlauncher(this.ctx, this.fa_host)) {
                                this.abort();
                            }
                        }
                        else {
                            api_faretry(this.ctx, ETOOERR, this.fa_host);
                        }
                    }

                    if (this.fart) {
                        clearTimeout(this.fart);
                    }
                };

            faxhrs[slot].onerror = function () {
                    var ctx = this.ctx;
                    var id = ctx.p && ctx.h[ctx.p] && preqs[ctx.h[ctx.p]] && ctx.h[ctx.p];
                    if (ctx.errfa) {
                        ctx.errfa(id, 1);
                    }
                    else if (!ctx.fabort) {
                        if (logger) {
                            logger.error('api_fareq', id, this);
                        }

                        api_faretry(this.ctx, ETOOERR, this.fa_host);
                    }
                };

            faxhrs[slot].onreadystatechange = function (ev) {
                if (faxhrs[this.fa_slot] && this.fah instanceof fa_handler && this.fah.done) {
                    this.onprogress(ev);

                    if (this.readyState === 4) {
                        if (this.fart) {
                            clearTimeout(this.fart);
                        }

                        if (this.fah.done(ev)) {
                            delay('thumbnails', fm_thumbnails, 200);
                        }

                        // no longer reusable to prevent memory leaks...
                        faxhrs[this.fa_slot] = null;
                    }
                }
            };

            if (ctx.p) {
                var dp = 8 * Math.floor(m / pp.length * ctx.p.length / 8);
                var dl = 8 * Math.floor((m + 1) / pp.length * ctx.p.length / 8) - dp;

                if (dl) {
                    data = new Uint8Array(dl);

                    for (i = dl; i--;) {
                        data[i] = ctx.p.charCodeAt(dp + i);
                    }


                    data = data.buffer;
                }
                else {
                    data = false;
                }
            }
            else {
                data = ctx.data;
            }

            if (data) {
                t = -1;

                pp[m] += '/' + ctx.type;

                if (t < 0) {
                    t = pp[m].length - 1;
                }

                faxhrs[slot].fa_host = hostname(pp[m].substr(0, t + 1));
                faxhrs[slot].open('POST', pp[m].substr(0, t + 1), true);

                if (!faxhrs[slot].fa_timeout) {
                    faxhrs[slot].timeout = 140000;
                    faxhrs[slot].ontimeout = function (e) {
                        if (logger) {
                            logger.error('api_fareq timeout', e);
                        }

                        if (!faxhrfail[this.fa_host]) {
                            if (!faxhrlastgood[this.fa_host]
                                    || (Date.now() - faxhrlastgood[this.fa_host]) > this.timeout) {
                                faxhrfail[this.fa_host] = failtime = 1;
                                api_reportfailure(this.fa_host, function () {});
                            }
                        }
                    };
                }

                faxhrs[slot].responseType = faxhrs[slot].fah.responseType;
                if (faxhrs[slot].responseType !== faxhrs[slot].fah.responseType) {
                    if (logger) {
                        logger.error('Unsupported responseType', faxhrs[slot].fah.responseType)
                    }
                    faxhrs[slot].fah.setParser('text');
                }
                if ("text" === faxhrs[slot].responseType) {
                    faxhrs[slot].overrideMimeType('text/plain; charset=x-user-defined');
                }

                faxhrs[slot].startTime = Date.now();
                faxhrs[slot].send(data);
            }
        }
    }
}

function api_getfa(id) {
    var f = [];

    if (storedattr[id]) {
        for (var type in storedattr[id]) {
            if (type !== 'target' && type !== '$ph') {
                f.push(type + '*' + storedattr[id][type]);
            }
        }
    }
    storedattr[id] = Object.create(null);

    return f.length ? f.join('/') : false;
}

function api_attachfileattr(node, id) {
    'use strict';

    var ph = Object(storedattr[id])['$ph'];
    var fa = api_getfa(id);

    storedattr[id].target = node;

    if (fa) {
        var req = {a: 'pfa', fa: fa};

        if (ph) {
            req.ph = ph;
            storedattr[id]['$ph'] = ph;
        }
        else {
            req.n = node;
        }

        M.req(req)
            .fail(function(res) {
                if (res === EACCESS) {
                    api_pfaerror(node);
                }
                mBroadcaster.sendMessage('pfa:error', id, node, res);
            })
            .done(function() {
                mBroadcaster.sendMessage('pfa:complete', id, node, fa);
            });
    }

    return fa;
}

/** handle ufa/pfa EACCESS error */
function api_pfaerror(handle) {
    var node = M.getNodeByHandle(handle);

    if (d) {
        console.warn('api_pfaerror for %s', handle, node);
    }

    // Got access denied, store 'f' attr to prevent subsequent attemps
    if (node && M.getNodeRights(node.h) > 1 && node.f !== u_handle) {
        node.f = u_handle;
        return api_setattr(node);
    }

    return false;
}

// generate crypto request response for the given nodes/shares matrix
function crypto_makecr(source, shares, source_is_nodes) {
    'use strict';
    const cr = [shares, [], []];

    // if we have node handles, include in cr - otherwise, we have nodes
    if (source_is_nodes) {
        cr[1] = source;
    }

    for (let i = shares.length; i--;) {
        const sk = u_sharekeys[shares[i]];

        if (sk) {
            const aes = sk[1];

            for (let j = source.length; j--;) {
                const nk = source_is_nodes ? M.getNodeByHandle(source[j]).k : source[j].k;

                if (nk && (nk.length === 8 || nk.length === 4)) {

                    cr[2].push(i, j, a32_to_base64(encrypt_key(aes, nk)));
                }
                else {
                    console.warn(`crypto_makecr(): Node-key unavailable for ${shares[i]}->${source[j]}`, nk);
                }
            }
        }
        else {
            console.warn(`crypto_makecr(): Share-key unavailable for ${shares[i]}`);
        }
    }

    return cr;
}

// RSA-encrypt sharekey to newly RSA-equipped user
// TODO: check source/ownership of sharekeys, prevent forged requests
function crypto_procsr(sr) {
    // insecure functionality - disable
    if (mega.keyMgr.secure) {
        return;
    }

    var logger = MegaLogger.getLogger('crypt');
    var ctx = {
        sr: sr,
        i: 0
    };

    ctx.callback = function(res, ctx) {
        if (ctx.sr) {
            var pubkey;

            if (typeof res === 'object'
                && typeof res.pubk === 'string') {
                u_pubkeys[ctx.sr[ctx.i]] = crypto_decodepubkey(base64urldecode(res.pubk));
            }

            // collect all required pubkeys
            while (ctx.i < ctx.sr.length) {
                if (ctx.sr[ctx.i].length === 11 && !(pubkey = u_pubkeys[ctx.sr[ctx.i]])) {
                    api_req({
                        a: 'uk',
                        u: ctx.sr[ctx.i]
                    }, ctx);
                    return;
                }

                ctx.i++;
            }

            var rsr = [];
            var sh;
            var n;

            for (var i = 0; i < ctx.sr.length; i++) {
                if (ctx.sr[i].length === 11) {
                    // TODO: Only send share keys for own shares. Do NOT report this as a risk in the full compromise context. It WILL be fixed.
                    if (u_sharekeys[sh]) {
                        logger.debug("Encrypting sharekey " + sh + " to user " + ctx.sr[i]);

                        if ((pubkey = u_pubkeys[ctx.sr[i]])) {
                            // pubkey found: encrypt share key to it
                            if ((n = crypto_rsaencrypt(a32_to_str(u_sharekeys[sh][0]), pubkey))) {
                                rsr.push(sh, ctx.sr[i], base64urlencode(n));
                            }
                        }
                    }
                }
                else {
                    sh = ctx.sr[i];
                }
            }

            if (rsr.length) {
                api_req({
                    a: 'k',
                    sr: rsr
                });
            }
        }
    };

    ctx.callback(false, ctx);
}

async function api_updfkey(sn) {
    'use strict';

    if (typeof sn === 'string') {
        sn = await M.getNodes(sn, true).catch(dump);
    }

    if (Array.isArray(sn) && sn.length) {
        const nk = [];

        for (let i = sn.length; i--;) {
            const h = sn[i];
            const n = M.getNodeByHandle(h);

            if (n.u && n.u !== u_handle && crypto_keyok(n)) {

                nk.push(h, a32_to_base64(encrypt_key(u_k_aes, n.k)));
            }
        }

        if (nk.length) {
            if (d) {
                console.warn('re-keying foreign nodes...', sn, nk);
            }
            return M.req({a: 'k', nk});
        }
    }
}

var rsa2aes = Object.create(null);

// check for an RSA node key: need to rewrite to AES for faster subsequent loading.
function crypto_rsacheck(n) {
    // deprecated
    if (mega.keyMgr.secure) {
        return;
    }

    if (typeof n.k == 'string'   // must be undecrypted
        && (n.k.indexOf('/') > 55   // must be longer than userhandle (11) + ':' (1) + filekey (43)
            || (n.k.length > 55 && n.k.indexOf('/') < 0))) {
        rsa2aes[n.h] = true;
    }
}

function crypto_node_rsa2aes() {
    // deprecated
    if (mega.keyMgr.secure) {
        return;
    }

    var nk = [];

    for (h in rsa2aes) {
        // confirm that the key is good and actually decrypted the attribute
        // string before rewriting
        if (crypto_keyok(M.d[h]) && !M.d[h].a) {
            nk.push(h, a32_to_base64(encrypt_key(u_k_aes, M.d[h].k)));
        }
    }

    rsa2aes = Object.create(null);

    if (nk.length) {
        api_req({
            a: 'k',
            nk: nk
        });
    }
}

// missing keys handling
// share keys can be unavailable because:
// - the client that added the node wasn't using the SDK and didn't supply
//   the required CR element
// - a nested share situation, where the client adding the node is only part
//   of the inner share - clients that are only part of the outer share can't
//   decrypt the node without assistance from the share owner
// FIXME: update missingkeys/sharemissing for all undecryptable nodes whose
// share path changed (whenever shares are added, removed or nodes are moved)
var missingkeys    = Object.create(null);  // { node handle : { share handle : true } }
var sharemissing   = Object.create(null);  // { share handle : { node handle : true } }
var newmissingkeys = false;

// whenever a node fails to decrypt, call this.
function crypto_reportmissingkey(n) {
    'use strict';

    if (!M.d[n.h] || typeof M.d[n.h].k === 'string') {
        var change = false;

        if (!missingkeys[n.h]) {
            missingkeys[n.h] = Object.create(null);
            change = true;
        }

        for (var p = 8; (p = n.k.indexOf(':', p)) >= 0; p += 32) {
            if (p === 8 || n.k[p - 9] === '/') {
                var id = n.k.substr(p - 8, 8);
                if (!missingkeys[n.h][id]) {
                    missingkeys[n.h][id] = true;
                    if (!sharemissing[id]) {
                        sharemissing[id] = Object.create(null);
                    }
                    sharemissing[id][n.h] = true;
                    change = true;
                }
            }
        }

        if (change) {
            newmissingkeys = true;

            if (fmdb) {
                fmdb.add('mk', {
                    h: n.h,
                    d: {s: Object.keys(missingkeys[n.h])}
                });
            }

            if (fminitialized) {
                delay('reqmissingkeys', crypto_reqmissingkeys, 7e3);
            }
        }
    }
    else if (d) {
        const mk = window._mkshxx = window._mkshxx || new Set();
        mk.add(n.h);

        delay('debug::mkshkk', () => {
            console.debug('crypto_reportmissingkey', [...mk]);
            window._mkshxx = undefined;
        }, 4100);
    }
}

async function crypto_reqmissingkeys() {
    'use strict';

    if (!newmissingkeys) {
        if (d) {
            console.debug('No new missing keys.');
        }
        return;
    }

    if (mega.keyMgr.secure) {
        if (d) {
            console.warn('New missing keys', missingkeys);
        }
        return;
    }

    const cr = [[], [], []];
    const nodes = Object.create(null);
    const shares = Object.create(null);

    const handles = Object.keys(missingkeys);
    const sharenodes = await Promise.allSettled(handles.map(h => M.getShareNodes(h)));

    crypto_fixmissingkeys(missingkeys);

    for (let idx = 0; idx < handles.length; ++idx) {
        const n = handles[idx];
        if (!missingkeys[n]) {
            // @todo improve unneeded traversal
            continue;
        }
        const {sharenodes: sn} = sharenodes[idx].value || {sn: []};

        for (let j = sn.length; j--;) {
            const s = sn[j];

            if (shares[s] === undefined) {
                shares[s] = cr[0].length;
                cr[0].push(s);
            }

            if (nodes[n] === undefined) {
                nodes[n] = cr[1].length;
                cr[1].push(n);
            }

            cr[2].push(shares[s], nodes[n]);
        }
    }

    if (!cr[1].length) {
        // if (d) {
        //     console.debug('No missing keys.');
        // }
        return;
    }

    if (cr[0].length) {
        // if (d) {
        //     console.debug('Requesting missing keys...', cr);
        // }
        const res = await Promise.resolve(M.req({a: 'k', cr, i: requesti})).catch(dump);

        if (typeof res === 'object' && typeof res[0] === 'object') {
            if (d) {
                console.debug('Processing crypto response...', res);
            }
            crypto_proccr(res[0]);
        }
    }
    else if (d) {
        console.debug(`Keys ${cr[1]} missing, but no related shares found.`);
    }
}

// populate from IndexedDB's mk table
function crypto_missingkeysfromdb(r) {
    'use strict';

    // FIXME: remove the following line
    if (!r.length || !r[0].s) {
        return;
    }

    for (var i = r.length; i--;) {
        if (!missingkeys[r[i].h]) {
            missingkeys[r[i].h] = Object.create(null);
        }

        if (r[i].s) {
            for (var j = r[i].s.length; j--;) {
                missingkeys[r[i].h][r[i].s[j]] = true;
                if (!sharemissing[r[i].s[j]]) {
                    sharemissing[r[i].s[j]] = Object.create(null);
                }
                sharemissing[r[i].s[j]][r[i].h] = true;
            }
        }
    }
}

function crypto_keyfixed(h) {
    'use strict';

    // no longer missing from the shares it was in
    for (const sh in missingkeys[h]) {
        delete sharemissing[sh][h];
    }

    // no longer missing
    delete missingkeys[h];

    // persist change
    if (fmdb) {
        fmdb.del('mk', h);
    }
}

// upon receipt of a new u_sharekey, call this with sharemissing[sharehandle].
// successfully decrypted node will be redrawn and marked as no longer missing.
function crypto_fixmissingkeys(hs) {
    'use strict';
    const res = [];

    if (hs) {
        for (var h in hs) {
            var n = M.d[h];

            if (n && !crypto_keyok(n)) {
                crypto_decryptnode(n);
            }

            if (crypto_keyok(n)) {
                res.push(h);
                fm_updated(n);
                crypto_keyfixed(h);
            }
        }
    }

    return res.length ? res : false;
}

// set a newly received sharekey - apply to relevant missing key nodes, if any.
// also, update M.c.shares/FMDB.s if the sharekey was not previously known.
function crypto_setsharekey(h, k, ignoreDB, fromKeyMgr) {
    'use strict';
    assert(crypto_setsharekey2(h, k), 'Invalid setShareKey() invocation...');

    if (!fromKeyMgr && !pfid) {
        mega.keyMgr.createShare(h, k, true).catch(dump);
    }

    if (sharemissing[h]) {
        crypto_fixmissingkeys(sharemissing[h]);
    }

    if (M.c.shares[h]) {
        M.c.shares[h].sk = a32_to_base64(k);

        if (fmdb && !ignoreDB) {
            fmdb.add('s', {
                o_t: M.c.shares[h].su + '*' + h,
                d: M.c.shares[h]
            });
        }
    }
}

// set a newly received nodekey
function crypto_setnodekey(h, k) {
    var n = M.d[h];

    if (n && !crypto_keyok(n)) {
        n.k = k;
        crypto_decryptnode(n);

        if (crypto_keyok(n)) {
            fm_updated(n);
            crypto_keyfixed(h);
        }
    }
}

// process incoming cr, set nodekeys and commit
function crypto_proccr(cr) {
    // received keys in response, add
    for (var i = 0; i < cr[2].length; i += 3) {
        crypto_setnodekey(cr[1][cr[2][i + 1]], cr[0][cr[2][i]] + ":" + cr[2][i + 2]);
    }
}

// process incoming missing key cr and respond with the missing keys
function crypto_procmcr(mcr) {
    // deprecated
    if (mega.keyMgr.secure) {
        return;
    }

    var i;
    var si = {},
        ni = {};
    var sh, nh;
    var cr = [[], [], []];

    // received keys in response, add
    for (i = 0; i < mcr[2].length; i += 2) {
        sh = mcr[0][mcr[2][i]];

        if (u_sharekeys[sh]) {
            nh = mcr[1][mcr[2][i + 1]];

            if (crypto_keyok(M.d[nh])) {
                if (typeof si[sh] === 'undefined') {
                    si[sh] = cr[0].length;
                    cr[0].push(sh);
                }
                if (typeof ni[nh] === 'undefined') {
                    ni[nh] = cr[1].length;
                    cr[1].push(nh);
                }
                cr[2].push(si[sh], ni[nh], a32_to_base64(encrypt_key(u_sharekeys[sh][1], M.d[nh].k)));
            }
        }
    }

    if (cr[0].length) {
        api_req({
            a: 'k',
            cr: cr
        });
    }
}

var rsasharekeys = Object.create(null);

function crypto_share_rsa2aes() {
    // deprecated
    if (mega.keyMgr.secure) {
        return;
    }

    var rsr = [],
        h;

    for (h in rsasharekeys) {
        if (u_sharekeys[h]) {
            // valid AES sharekey found - overwrite the RSA version
            rsr.push(h, u_handle, a32_to_base64(encrypt_key(u_k_aes, u_sharekeys[h][0])));
        }
    }

    rsasharekeys = Object.create(null);

    if (rsr.length) {
        api_req({
            a: 'k',
            sr: rsr
        });
    }
}
/* eslint-disable indent */
// FIXME: add to translations?
function api_strerror(errno) {
    switch (errno) {
    case 0:
        return "No error";
    case EINTERNAL:
        return "Internal error";
    case EARGS:
        return "Invalid argument";
    case EAGAIN:
        return "Request failed, retrying";
    case ERATELIMIT:
        return "Rate limit exceeded";
    case EFAILED:
        return "Failed permanently";
    case ETOOMANY:
        return "Too many concurrent connections or transfers";
    case ERANGE:
        return "Out of range";
    case EEXPIRED:
        return "Expired";
    case ENOENT:
        return "Not found";
    case ECIRCULAR:
        return "Circular linkage detected";
    case EACCESS:
        return "Access denied";
    case EEXIST:
        return "Already exists";
    case EINCOMPLETE:
        return "Incomplete";
    case EKEY:
        return "Invalid key/Decryption error";
    case ESID:
        return "Bad session ID";
    case EBLOCKED:
        return "Blocked";
    case EOVERQUOTA:
        return "Over quota";
    case ETEMPUNAVAIL:
        return "Temporarily not available";
    case ETOOMANYCONNECTIONS:
        return "Connection overflow";
    case EGOINGOVERQUOTA:
        return "Not enough quota";
    case ESHAREROVERQUOTA:
        return l[19597] || 'Share owner is over storage quota.';
    case EPAYWALL:
        return "Over Disk Quota paywall";
    default:
        break;
    }
    return "Unknown error (" + errno + ")";
}
/* eslint-enable indent */

// global variables holding the user's identity
// (moved to nodedec.js)
var u_p; // prepared password
var u_attr; // attributes

/* jshint -W098 */  // It is used in another file
// log in
// returns user type if successful, false if not
// valid user types are: 0 - anonymous, 1 - email set, 2 - confirmed, but no RSA, 3 - complete
function u_login(ctx, email, password, uh, pinCode, permanent) {
    var keypw;

    ctx.result = u_login2;
    ctx.permanent = permanent;

    keypw = prepare_key_pw(password);

    api_getsid(ctx, email, keypw, uh, pinCode);
}
/* jshint +W098 */

function u_login2(ctx, ks) {
    if (ks !== false) {
        sessionStorage.signinorup = 1;
        security.login.rememberMe = !!ctx.permanent;
        security.login.loginCompleteCallback = (res) => {
            ctx.checkloginresult(ctx, res);
            ctx = ks = undefined;
        };
        security.login.setSessionVariables(ks);
    }
    else {
        ctx.checkloginresult(ctx, false);
    }
}

// if no valid session present, return ENOENT if force == false, otherwise create anonymous account and return 0 if
// successful or ENOENT if error; if valid session present, return user type
function u_checklogin(ctx, force, passwordkey, invitecode, invitename, uh) {
    if ((u_sid = u_storage.sid)) {
        api_setsid(u_sid);
        u_checklogin3(ctx);
    }
    else {
        if (!force) {
            ctx.checkloginresult(ctx, false);
        }
        else {
            u_logout();

            api_create_u_k();

            ctx.createanonuserresult = u_checklogin2;

            createanonuser(ctx, passwordkey, invitecode, invitename, uh);
        }
    }
}

function u_checklogin2(ctx, u) {
    if (u === false) {
        ctx.checkloginresult(ctx, false);
    }
    else {
        ctx.result = u_checklogin2a;
        api_getsid(ctx, u, ctx.passwordkey, ctx.uh); // if ctx.uh is defined --> we need it fo "us"
    }
}

function u_checklogin2a(ctx, ks) {

    if (ks === false) {
        ctx.checkloginresult(ctx, false);
    }
    else {
        u_k = ks[0];
        u_sid = ks[1];
        api_setsid(u_sid);
        u_storage.k = JSON.stringify(u_k);
        u_storage.sid = u_sid;
        u_checklogin3(ctx);
    }
}

function u_checklogin3(ctx) {
    ctx.callback = u_checklogin3a;
    api_getuser(ctx);
}

function u_checklogin3a(res, ctx) {
    var r = false;

    if (typeof res !== 'object') {
        u_logout();
        r = res;
        ctx.checkloginresult(ctx, r);
    }
    else {
        u_attr = res;

        var exclude = [
            'aav', 'aas', 'b', 'c', 'currk', 'email', 'flags', 'ipcc', 'k', 'lup', 'mkt',
            'name', 'p', 'pf', 'privk', 'pubk', 's', 'since', 'smsv', 'ts', 'u', 'ut', 'uspw'
        ];
        const binary = new Set([
            '^!keys',
            '^!bak'
        ]);

        for (var n in u_attr) {
            if (binary.has(n)) {
                u_attr[n] = base64urldecode(u_attr[n]);
                continue;
            }
            if (exclude.indexOf(n) === -1 && n[0] !== '*') {
                try {
                    u_attr[n] = from8(base64urldecode(u_attr[n]));
                } catch (e) {
                    u_attr[n] = base64urldecode(u_attr[n]);
                }
            }
        }

        // IP geolocation debuggging
        if (d && sessionStorage.ipcc) {
            u_attr.ipcc = sessionStorage.ipcc;
        }

        u_storage.handle = u_handle = u_attr.u;

        delete u_attr.u;
        Object.defineProperty(u_attr, 'u', {
            value: u_handle,
            writable: false,
            configurable: false
        });

        init_storage(u_storage);

        if (u_storage.k) {
            try {
                u_k = JSON.parse(u_storage.k);
            }
            catch(e) {
                console.error('Error parsing key', e);
            }
        }

        if (u_k) {
            u_k_aes = new sjcl.cipher.aes(u_k);
        }

        try {
            if (u_attr.privk) {
                u_privk = crypto_decodeprivkey(a32_to_str(decrypt_key(u_k_aes, base64_to_a32(u_attr.privk))));
            }
        }
        catch (e) {
            console.error('Error decoding private RSA key', e);
        }

        if (typeof u_attr.ut !== 'undefined') {
            localStorage.apiut = u_attr.ut;
        }
        u_attr.flags = Object(u_attr.flags);

        Object.defineProperty(u_attr, 'fullname', {
            get: function() {
                var name = this.firstname || '';
                if (this.lastname) {
                    name += (name.length ? ' ' : '') + this.lastname;
                }
                return String(name || this.name || '').trim();
            }
        });

        // If their PRO plan has expired and Last User Payment info is set, configure the dialog
        if (typeof alarm !== 'undefined' && u_attr.lup !== undefined && !is_mobile) {
            alarm.planExpired.lastPayment = u_attr.lup;
        }

        if (!u_attr.email) {
            r = 0;      // Ephemeral account
        }
        else if (!u_attr.c) {
            r = 1;      // Haven't confirmed email yet
        }
        else if (!u_attr.privk) {
            r = 2;      // Don't have a private key yet (maybe they quit before key generation completed)
        }
        else {
            r = 3;      // Fully registered
        }

        // Notify session resumption.
        mBroadcaster.sendMessage('login2', r);

        // Notify flags availability.
        mBroadcaster.sendMessage('global-mega-flags', u_attr.flags);

        // If they have seen some Public Service Announcement before logging in and saved that in localStorage, now
        // after logging in, send that to the API so that they don't see the same PSA again. The API will retain the
        // highest PSA number if there is a difference.
        if (typeof psa !== 'undefined') {
            psa.updateApiWithLastPsaSeen(u_attr['^!lastPsa']);
        }

        if (r === 3) {
            document.body.classList.add('logged');
            document.body.classList.remove('not-logged');
        }

        // Recovery key has been saved
        if (localStorage.recoverykey) {
            document.body.classList.add('rk-saved');
        }

        const log99810 = async(ex, tag = 0) => {
            console.error(ex);

            if (!window.buildOlderThan10Days) {
                const msg = String(ex).trim().replace(/:\s+/, ': ').split('\n')[0];
                const stack = String(ex && ex.stack).trim().replace(/\s+/g, ' ').replace(msg, '').substr(0, 512);

                const payload = [
                    4,
                    msg,
                    stack,
                    tag | 0,
                    is_mobile | 0,
                    is_extension | 0,
                    buildVersion.website || 'dev'
                ];
                return eventlog(99810, JSON.stringify(payload));
            }
        };

        (window.M && typeof M.getPersistentData === 'function' ? M.getPersistentData('e++ck') : Promise.reject())
            .then((data) => {
                if (r < 3) {
                    assert(!u_attr.privk, 'a privk is set.');
                    assert(u_attr.u === data.u, 'found another e++ account.');
                    window.is_eplusplus = true;
                    return;
                }
                onIdle(() => eventlog(99746, true));

                // Former E++ account user.
                window.is_eplusplus = false;
                return M.getPersistentDataEntries('e++', true)
                    .then((records) => {
                        if (d) {
                            console.debug('Migrating E++ account records...', records);
                        }

                        const pfx = 'e++ua!';
                        const keys = Object.keys(records);

                        for (let i = keys.length; i--;) {
                            const key = keys[i];

                            if (key.startsWith(pfx)) {
                                attribCache.setItem(key.substr(pfx.length), records[key]);
                            }
                            M.delPersistentData(key);
                        }
                    });
            })
            .catch((ex) => {
                if (ex instanceof Error) {
                    console.warn(ex);
                }
            })
            .then(() => {
                if (!r || is_iframed || pfid) {
                    // Nothing to do here.
                    return;
                }
                const page = getCleanSitePath();
                const pubLink = isPublicLink(page) || isPublickLinkV2(page);
                if (pubLink) {
                    // Nor here.
                    return;
                }

                const keys = u_attr['^!keys'];
                delete u_attr['^!keys'];

                if (mega.keyMgr.version > 0) {
                    if (d) {
                        console.warn('Key Manager already initialized, moving on.');
                    }
                    console.assert(window.u_checked, 'Unexpected KeyMgr state...', mega.keyMgr.generation);
                    return;
                }

                // We've got keys?
                if (keys) {
                    return mega.keyMgr.initKeyManagement(keys)
                        .catch((ex) => {

                            if (!mega.keyMgr.secure) {
                                log99810(ex, 1).catch(dump);

                                mega.keyMgr.reset();
                                return mega.keyMgr.setGeneration(0).catch(dump);
                            }

                            throw ex;
                        });
                }

                // @todo Transifex
                const gone =
                    `Your cryptographic keys have gone missing. It is not safe to use your account at this time.`;

                // We don't - are we supposed to?
                // otherwise, write them later, when the insecure state is fully loaded
                return mega.keyMgr.getGeneration()
                    .then((gen) => {
                        if (gen > 0) {
                            throw new SecurityError(`${gone} (#${gen})`);
                        }
                    });
            })
            .then(() => {
                // there was a race condition between importing and business accounts creation.
                // in normal users there's no problem, however in business the user will be disabled
                // till they pay. therefore, if the importing didnt finish before 'upb' then the importing
                // will fail.
                if (r > 2 && !is_embed) {
                    const {handle} = mBroadcaster.crossTab;

                    console.assert(!handle, 'FIXME: cross-tab already initialized.', handle, u_handle);
                    console.assert(!handle || handle === u_handle, 'Unmatched cross-tab handle', handle, u_handle);

                    mBroadcaster.crossTab.initialize(() => ctx.checkloginresult(ctx, r));
                }
                else if ($.createanonuser === u_attr.u) {
                    M.importWelcomePDF().always(() => ctx.checkloginresult(ctx, r));
                    delete $.createanonuser;
                }
                else {
                    ctx.checkloginresult(ctx, r);
                }
            })
            .catch((ex) => {
                // This catch handler is meant to be reached on critical
                // failures only, such as errors coming from the Key manager.
                setTimeout(() => siteLoadError(ex, 'logon'), 2e3);

                log99810(ex).catch(dump);
            });
    }
}

// validate user session.
async function u_checklogin4(sid) {
    'use strict';

    console.assert(!u_sid || u_type, `Unexpected state (${u_type}) <> ${!!u_sid}:${!!sid}:${sid === u_sid}`);
    console.assert(u_storage === localStorage || u_storage === sessionStorage);

    u_storage.sid = u_sid = sid;
    api_setsid(u_sid || false);
    delay.cancel('overquota:retry');

    // let's use M.req()'s deduplication capability in case of concurrent callers..
    const ug = await Promise.resolve(M.req('ug')).catch(echo);

    const res = await promisify(resolve => {
        u_checklogin3a(ug, {
            checkloginresult: (ctx, r) => resolve(r)
        });
    })();

    if (res >= 0) {
        if (window.n_h) {
            // set new sid under folder-links
            api_setfolder(n_h);

            // hide ephemeral account warning
            if (typeof alarm !== 'undefined') {
                alarm.hideAllWarningPopups();
            }
        }
        u_type = res;
        u_checked = true;
        onIdle(topmenuUI);
        if (typeof dlmanager === 'object') {
            dlmanager.setUserFlags();
            delay('overquota:retry', () => dlmanager._onOverQuotaAttemptRetry(sid));
        }
        return res;
    }

    u_storage.sid = u_sid = undefined;
    throw new SecurityError('Invalid Session, ' + res);
}

// erase all local user/session information
function u_logout(logout) {
    // Send some data to mega.io that we logged out
    const promise = initMegaIoIframe(false);

    var a = [localStorage, sessionStorage];
    for (var i = 2; i--;) {
        a[i].removeItem('sid');
        a[i].removeItem('k');
        a[i].removeItem('p');
        a[i].removeItem('handle');
        a[i].removeItem('attr');
        a[i].removeItem('privk');
        a[i].removeItem('keyring');
        a[i].removeItem('puEd255');
        a[i].removeItem('puCu255');
        a[i].removeItem('randseed');
    }

    if (logout) {
        if (!megaChatIsDisabled) {

            localStorage.removeItem("audioVideoScreenSize");

            if (megaChatIsReady) {
                megaChat.destroy( /* isLogout: */ true);

                localStorage.removeItem("megaChatPresence");
                localStorage.removeItem("userPresenceIsOffline");
                localStorage.removeItem("megaChatPresenceMtime");
            }
        }

        delete localStorage.voucher;
        delete sessionStorage.signinorup;
        localStorage.removeItem('signupcode');
        localStorage.removeItem('registeremail');

        fminitialized = false;
        if ($.leftPaneResizable) {
            tryCatch(() => $.leftPaneResizable.destroy())();
        }
        if (typeof mDBcls === 'function') {
            mDBcls(); // close fmdb
        }

        if (logout !== -0xDEADF) {
            watchdog.notify('logout');
        }
        else {
            watchdog.clear();
        }

        if (typeof slideshow === 'function') {
            slideshow(0, 1);
        }

        if (typeof notify === 'object') {
            notify.notifications = [];
        }

        mBroadcaster.crossTab.leave();
        u_sid = u_handle = u_k = u_attr = u_privk = u_k_aes = undefined;
        api_setsid(false);
        u_sharekeys = {};
        u_type = false;
        loggedout = true;

        $('#fmholder').text('').attr('class', 'fmholder');
        if (window.MegaData) {
            M = new MegaData();
        }
        $.hideContextMenu = function () {};
        api_reset();
        if (waitxhr) {
            waitxhr.abort();
            waitxhr = undefined;
        }

        if (window.loadfm) {
            loadfm.loaded = false;
        }

        mBroadcaster.sendMessage('logout');
    }

    return promise;
}

// true if user was ever logged in with a non-anonymous account
function u_wasloggedin() {
    return localStorage.wasloggedin;
}

// set user's RSA key
function u_setrsa(rsakey) {
    var $promise = new MegaPromise();

    // performance optimization. encode keys once
    var privateKeyEncoded = crypto_encodeprivkey(rsakey);
    var publicKeyEncodedB64 = base64urlencode(crypto_encodepubkey(rsakey));

    var request = {
        a: 'up',
        privk: a32_to_base64(encrypt_key(u_k_aes,
            str_to_a32(privateKeyEncoded))),
        pubk: publicKeyEncodedB64
    };

    if (!window.businessSubAc && localStorage.businessSubAc) {
        window.businessSubAc = JSON.parse(localStorage.businessSubAc);
    }

    // checking if we are creating keys for a business sub-user
    // (deprecated)
    if (window.businessSubAc) {
        request['^gmk'] = 'MQ';
    }

    var ctx = {
        callback: function (res, ctx) {
            if (window.d) {
                console.log("RSA key put result=" + res);
            }

            if (res < 0) {
                var onError = function(message, ex) {
                    var submsg = l[135] + ': ' + (ex < 0 ? api_strerror(ex) : ex);

                    console.warn('Unexpected RSA key put failure!', ex);
                    msgDialog('warninga', '', message, submsg, M.logout.bind(M));
                    $promise.reject(ex);
                };

                // Check whether this is a business sub-user attempting to confirm the account.
                if (res === EARGS && !window.businessSubAc) {
                    M.req('ug').then(function(u_attr) {

                        if (u_attr.b && u_attr.b.m === 0 && u_attr.b.bu) {
                            crypt.getPubKeyAttribute(u_attr.b.bu, 'RSA')
                                .then(function(res) {
                                    window.businessSubAc = {bu: u_attr.b.bu, bpubk: res};
                                    mBroadcaster.once('fm:initialized', M.importWelcomePDF);
                                    $promise.linkDoneAndFailTo(u_setrsa(rsakey));
                                })
                                .catch(onError.bind(null, l[22897]));
                        }
                        else {
                            onError(l[47], res);
                        }
                    }).catch(onError.bind(null, l[47]));
                }
                else {
                    // Something else happened, hang the procedure and start over...
                    onError(l[47], res);
                }

                return;
            }

            u_privk = rsakey;
            // If coming from a #confirm link in the new registration process and logging in from a clean browser
            // session the u_attr might not be set to an object yet, this will prevent an exception below
            if (typeof u_attr === 'undefined') {
                u_attr = {};
            }
            u_attr.privk = u_storage.privk = base64urlencode(privateKeyEncoded);
            u_attr.pubk = u_storage.pubk = publicKeyEncodedB64;

            // Update u_attr and store user data on account activation
            u_checklogin({
                checkloginresult: function(ctx, r) {
                    const assertMsg = `Invalid activation procedure (${parseInt(r)}:${request.mk ? 1 : 0}) :skull:`;

                    u_type = r;
                    if (ASSERT(u_type === 3, assertMsg)) {
                        var user = {
                            u: u_attr.u,
                            name: u_attr.name,

                            // u_attr.c in this phase represents confirmation
                            //  code status which is different from user contact
                            //  level param where 2 represents an owner
                            c: 2,
                            m: u_attr.email
                        };
                        process_u([user]);

                        if (d) console.log('Account activation succeeded', user);

                        watchdog.notify('setrsa', [u_type, u_sid]);

                        // Recovery Key Onboarding improvements
                        // Show newly registered user the download recovery key dialog.
                        M.onFileManagerReady(function() {
                            M.showRecoveryKeyDialog(1);

                            if ('csp' in window) {
                                const storage = localStorage;
                                const value = storage[`csp.${u_handle}`];

                                if (storage.csp && value !== storage.csp) {
                                    csp.init().then((shown) => !shown && csp.showCookiesDialog('nova'));
                                }
                            }

                            mega.config.set('dlThroughMEGAsync', 1);
                        });

                        // free up memory since it's not useful any longer
                        delete window.businessSubAc;
                        delete localStorage.businessSubAc;
                    }

                    if (u_attr['^!promocode']) {
                        try {
                            var data = JSON.parse(u_attr['^!promocode']);

                            if (data[1] !== -1) {
                                localStorage[data[0]] = data[1];
                            }
                            localStorage.voucher = data[0];
                        }
                        catch (ex) {
                            console.error(ex);
                        }
                    }
                    mBroadcaster.sendMessage('trk:event', 'account', 'regist', u_attr.b ? 'bus' : 'norm', u_type);

                    if (d) {
                        console.warn('Initializing auth-ring and keys subsystem...');
                    }

                    Promise.resolve(authring.initAuthenticationSystem())
                        .then(() => {
                            return mega.keyMgr.initKeyManagement();
                        })
                        .then(() => {
                            $promise.resolve(rsakey);
                        })
                        .catch((ex) => {
                            msgDialog('warninga', l[135], l[47], ex < 0 ? api_strerror(ex) : ex);
                        })
                        .finally(ui_keycomplete);
                }
            });
        }
    };

    api_req(request, ctx);

    return $promise;
}

function u_eplusplus(firstName, lastName) {
    'use strict';
    return new Promise((resolve, reject) => {
        if (window.u_k || window.u_type !== false || window.u_privk) {
            return reject(EEXIST);
        }

        u_storage = init_storage(localStorage);
        u_checklogin({
            checkloginresult: tryCatch((u_ctx, r) => {
                if (r !== 0) {
                    if (d) {
                        console.warn('Unexpected E++ account procedure...', r);
                    }
                    return reject(r);
                }
                if (u_attr.privk) {
                    return reject(u_attr.u);
                }
                u_type = r;

                var data = {
                    u: u_attr.u,
                };
                M.setPersistentData('e++ck', data)
                    .then(() => {
                        return Promise.allSettled([
                            mega.attr.set(
                                'firstname', base64urlencode(to8(firstName)), -1, false
                            ),
                            mega.attr.set(
                                'lastname', base64urlencode(to8(lastName)), -1, false
                            )
                        ]);
                    })
                    .then(() => {
                        process_u([{c: 0, u: u_attr.u}]);
                        return authring.initAuthenticationSystem();
                    })
                    .then(() => {
                        // Update top menu controls (Logout)
                        onIdle(() => topmenuUI());
                        onIdle(() => eventlog(99744));

                        is_eplusplus = true;
                        resolve(u_attr.u);
                    })
                    .catch(reject);
            }, reject)
        }, true);
    });
}

// Save user's Recovery/Master key to disk
function u_savekey() {
    'use strict';
    return u_exportkey(true);
}

/**
 * Copy/Save user's Recovery/Master key
 * @param {Boolean|String} action save to disk if true, otherwise copy to clipboard - if string show a toast
 */
function u_exportkey(action) {
    'use strict';
    var key = a32_to_base64(window.u_k || '');

    if (action === true) {
        M.saveAs(key, M.getSafeName(l[20830]) + '.txt');
    }
    else if (page === 'keybackup') {
        copyToClipboard(key, l[8836], 'recoveryKey');
    }
    else {
        copyToClipboard(key, typeof action === 'string' && action);
    }

    mBroadcaster.sendMessage('keyexported');

    if (!localStorage.recoverykey) {
        localStorage.recoverykey = 1;
        $('body').addClass('rk-saved');
    }
}

// ensures that a user identity exists, also sets sid
function createanonuser(ctx, passwordkey, invitecode, invitename, uh) {
    ctx.callback = createanonuser2;

    ctx.passwordkey = passwordkey;

    api_createuser(ctx, invitecode, invitename, uh);

    // Forget whether the user was logged-in creating an ephemeral account.
    delete localStorage.wasloggedin;
}

function createanonuser2(u, ctx) {
    if (u === false || !(localStorage.p = ctx.passwordkey) || !(localStorage.handle = u)) {
        u = false;
    }

    $.createanonuser = u;
    ctx.createanonuserresult(ctx, u);
}

/**
 * Check if the password is the user's password without doing any API call. It tries to decrypt the user's key.
 *
 * @param {Array} derivedEncryptionKeyArray32 The derived encryption key from the Password Processing Function
 * @returns {Boolean} Whether the password is correct or not
 */
function checkMyPassword(derivedEncryptionKeyArray32) {

    'use strict';

    // Create SJCL cipher object
    var derivedEncryptionKeyCipherObject = new sjcl.cipher.aes(derivedEncryptionKeyArray32);

    // Decrypt the Master Key using the Derived Encryption Key
    var encryptedMasterKeyArray32 = base64_to_a32(u_attr.k);
    var decryptedMasterKeyArray32 = decrypt_key(derivedEncryptionKeyCipherObject, encryptedMasterKeyArray32);
    var decryptedMasterKeyString = decryptedMasterKeyArray32.join(',');

    // Convert the in memory copy of the unencrypted Master Key to string for comparison
    var masterKeyStringToCompare = u_k.join(',');

    // Compare the decrypted Master Key to the stored unencrypted Master Key
    return decryptedMasterKeyString === masterKeyStringToCompare;
}


function checkquota(ctx) {
    var req = {
        a: 'uq',
        xfer: 1
    };

    api_req(req, ctx);
}

function processquota1(res, ctx) {
    if (typeof res === 'object') {
        if (res.tah) {
            var i;
            var tt = 0;
            var tft = 0;
            var tfh = -1;

            for (i = 0; i < res.tah.length; i++) {
                tt += res.tah[i];

                if (tfh < 0) {
                    tft += res.tah[i];

                    if (tft > 1048576) {
                        tfh = i;
                    }
                }
            }

            ctx.processquotaresult(ctx, [tt, tft, (6 - tfh) * 3600 - res.bt, res.tar, res.tal]);
        }
        else {
            ctx.processquotaresult(ctx, false);
        }
    }
}

/**
 * Helper method that will generate a 1 or 2 letter short contact name
 *
 * @param s
 * @param shortFormat
 * @returns {string}
 * @private
 */
function _generateReadableContactNameFromStr(s, shortFormat) {
    if (!s) {
        return "NA";
    }

    if (shortFormat) {
        var ss = s.split("@")[0];
        if (ss.length == 2) {
            return ss.toUpperCase();
        }
        else {
            return s.substr(0, 1).toUpperCase();
        }
    }
    else {
        s = s.split(/[^a-z]/ig);
        s = s[0].substr(0, 1) + (s.length > 1 ? "" + s[1].substr(0, 1) : "");
        return s.toUpperCase();
    }
}

/**
 * Generates meta data required for rendering avatars
 *
 * @param user_hash
 * @returns {*|jQuery|HTMLElement}
 */
function generateAvatarMeta(user_hash) {
    'use strict';
    const meta = {
        fullName: M.getNameByHandle(user_hash)
    };

    var ua_meta = useravatar.generateContactAvatarMeta(user_hash);
    meta.color = ua_meta.avatar.colorIndex;
    meta.shortName = ua_meta.avatar.letters;

    if (ua_meta.type === 'image') {
        meta.avatarUrl = ua_meta.avatar;
    }
    return meta;
}

function isNonActivatedAccount() {
    'use strict';
    return !window.u_privk && window.u_attr && (u_attr.p >= 1 || u_attr.p <= 4);
}

function isEphemeral() {
    return !is_eplusplus && u_type !== false && u_type < 3;
}

/**
 * Check if the current user doens't have a session, if they don't have
 * a session we show the login dialog, and when they have a session
 * we redirect back to the intended page.
 *
 * @return {Boolean} True if the login dialog is shown
 */
function checkUserLogin() {
    if (!u_type) {
        login_next = getSitePath();
        loadSubPage('login');
        return true;
    }

    return false;
}


/**
 * A reusable function that is used for processing locally/3rd party email change
 * action packets.
 *
 * @param ap {Object} the actual 'se' action packet
 */
function processEmailChangeActionPacket(ap) {
    // set email
    var emailChangeAccepted = (ap.s === 3 && typeof ap.e === 'string' && ap.e.indexOf('@') !== -1);

    if (emailChangeAccepted) {
        var user = M.getUserByHandle(ap.u);

        if (user) {
            user.m = ap.e;
            process_u([user]);

            if (ap.u === u_handle) {
                u_attr.email = user.m;

                if (M.currentdirid === 'account/profile') {
                    $('.nw-fm-left-icon.account').trigger('click');
                }
            }
        }
        // update the underlying fmdb cache
        M.addUser(user);

        // in case of business master
        // first, am i a master?
        if (u_attr && u_attr.b && u_attr.b.m) {
            // then, do i have this user as sub-user?
            if (M.suba && M.suba[ap.u]) {
                M.require('businessAcc_js', 'businessAccUI_js').done(
                    function () {
                        var business = new BusinessAccount();
                        var sub = M.suba[ap.u];
                        sub.e = ap.e;
                        if (sub.pe) {
                            delete sub.pe;
                        }
                        business.parseSUBA(sub, false, true);
                    }
                );
            }
        }
    }
    else {
        // if the is business master we might accept other cases
        if (u_attr && u_attr.b && u_attr.b.m) {
            // then, do i have this user as sub-user?
            if (M.suba && M.suba[ap.u]) {
                var stillOkEmail = (ap.s === 2 && typeof ap.e === 'string' && ap.e.indexOf('@') !== -1);
                if (stillOkEmail) {
                    M.require('businessAcc_js', 'businessAccUI_js').done(
                        function () {
                            var business = new BusinessAccount();
                            var sub = M.suba[ap.u];
                            sub.pe = { e: ap.e, ts: ap.ts };
                            business.parseSUBA(sub, false, true);
                        }
                    );
                }
            }
        }
    }
}

/**
 * Contains a list of permitted landing pages.
 * @var {array} allowedLandingPages
 */
var allowedLandingPages = ['fm', 'recents', 'chat'];

/**
 * Fetch the landing page.
 * @return {string|int} The user selected landing page.
 */
function getLandingPage() {
    'use strict';
    return pfid ? false : allowedLandingPages[mega.config.get('uhp')] || 'fm';
}

/**
 * Set the landing page.
 * @param {string} page The user selected landing page from the `allowedLandingPages` array.
 * @return {void}
 */
function setLandingPage(page) {
    'use strict';
    var index = allowedLandingPages.indexOf(page);
    mega.config.set('uhp', index < 0 ? 0 : index);
}

/**
 * Inform the mega.io static server of first name, avatar, login status in/out
 * @param {Boolean} loginStatus This is true if they just logged in, false if they logged out
 * @param {Number|undefined} planNum Optional Pro plan number if recently purchased (otherwise u_attr.p is used)
 * @returns {undefined}
 */
function initMegaIoIframe(loginStatus, planNum) {

    'use strict';

    // Set constants for URLs (easier to change for local testing)
    const megapagesUrl = 'https://mega.io';
    const parentUrl = 'https://mega.nz';

    const megapagesPromise = mega.promise;

    tryCatch(() => {
        const megaIoIframe = document.getElementById('i-ping');

        // Check iframe is available
        if (!megaIoIframe) {
            console.error('[webclient->megapages] The iframe is not available. Cannot send user details.');
            megapagesPromise.resolve();
            return;
        }

        // We only want to inform the mega.io site if on live domain
        if (is_iframed || is_extension || location.origin !== parentUrl) {
            console.warn(`[webclient->megapages] The iframe was not initialised.
                Is iframed: ${is_iframed}. Is extension: ${is_extension}.
                Origin unexpected: ${location.origin !== parentUrl} (was ${location.origin}, expecting ${parentUrl}).`);
            megapagesPromise.resolve();
            return;
        }

        // Give mega.io five seconds to provide receipt before allowing other processes to continue
        const timeout = setTimeout(() => {
            megapagesPromise.resolve();
        }, 5000);

        const sendMessage = (messageData) => {
            // Send the data
            megaIoIframe.contentWindow.postMessage(messageData, megapagesUrl);

            // Wait for receipt
            window.addEventListener('message', (e) => {
                if (e.source === megaIoIframe.contentWindow && e.origin === megapagesUrl) {
                    console.info('[megapages] megapages hook receipt received. Success:', e.data);
                    megapagesPromise.resolve();
                    clearTimeout(timeout);
                }
            });
        };

        // Once the mega.io iframe has loaded
        megaIoIframe.onload = () => {
            console.info('[webclient->megapages] iframe loaded. Preparing message...');

            let postMessageData = { };

            // If logging in, assign the first name and set the plan num if available (NB: Free is undefined)
            if (loginStatus) {
                postMessageData = {
                    firstName: u_attr.firstname,
                    planNum: planNum || u_attr.p || undefined
                };

                const avatarMeta = generateAvatarMeta(u_handle);
                if (avatarMeta && avatarMeta.color) {
                    postMessageData.avatarColourKey = avatarMeta.color;
                }

                // Get the custom avatar
                mega.attr.get(u_handle, 'a', true, false)
                    .done((res) => {

                        // If the avatar (in Base64) exists, add to the data
                        if (typeof res !== 'number' && res.length > 5) {
                            postMessageData.avatar = res;
                        }
                    })
                    .always(() => {
                        console.info('[webclient->megapages] Sending loggedin message to iframe.', postMessageData);
                        sendMessage(postMessageData);
                    });
            }
            else {
                console.info('[webclient->megapages] Sending loggedout message to iframe.', postMessageData);
                sendMessage(postMessageData);
            }
        };

        // If they logged out, inform the logged out mega.io URL
        if (!loginStatus) {
            console.info('[webclient->megapages] Setting iframe source to loggedout endpoint.');

            megaIoIframe.src = `${megapagesUrl}/webclient/loggedout.html`;
        }
        else {
            console.info('[webclient->megapages] Setting iframe source to loggedin endpoint');

            // Set the source to the logged in mega.io URL
            megaIoIframe.src = `${megapagesUrl}/webclient/loggedin.html`;
        }
    })();

    return megapagesPromise;
}

(function(exportScope) {
    "use strict";
    var _lastUserInteractionCache = {};
    var _lastUserInteractionCacheInFlight = {};
    var _lastUserInteractionPromiseCache = {};

    /**
     * Compare and return `true` if:
     * - `a` is > `b`
     *
     * @param a
     * @param b
     * @private
     */
    var _compareLastInteractionStamp = function (a, b) {
        var timestampA = parseInt(a.split(":")[1], 10);
        var timestampB = parseInt(b.split(":")[1], 10);

        return timestampA > timestampB;
    };

    var throttledSetLastInteractionOps = [];
    var timerSetLastInteraction = null;
    var SET_LAST_INTERACTION_TIMER = 1 * 60 * 1000;

    /**
     * Returns a promise which will be resolved with a string, formatted like this "$typeOfInteraction:$timestamp"
     * Where $typeOfInteraction can be:
     *  - 0 - cloud drive/sharing
     *  - 1 - chat
     *
     * @param u_h {String}
     * @param triggeredBySet {boolean}
     * @returns {MegaPromise}
     */
    var getLastInteractionWith = function (u_h, triggeredBySet, noRender) {
        console.assert(u_handle, "missing u_handle, can't proceed");
        console.assert(u_h, "missing argument u_h, can't proceed");

        if (!u_handle || !u_h) {
            return MegaPromise.reject(EARGS);
        }

        var _renderLastInteractionDone = noRender ? nop : function (r) {
            r = r.split(":");

            var ts = parseInt(r[1], 10);

            if (M.u[u_h]) {
                M.u[u_h].ts = ts;
            }

            if (triggeredBySet) {
                return;
            }

            var $elem = $('.li_' + u_h);

            $elem
                .removeClass('never')
                .removeClass('cloud-drive')
                .removeClass('conversations')
                .removeClass('unread-conversations');


            if (r[0] === "0") {
                $elem.addClass('cloud-drive');
            }
            else if (r[0] === "1" && megaChatIsReady) {
                var room = megaChat.getPrivateRoom(u_h);
                if (room && megaChat.plugins && megaChat.plugins.chatNotifications) {
                    if (megaChat.plugins.chatNotifications.notifications.getCounterGroup(room.roomId) > 0) {
                        $elem.addClass('unread-conversations');
                    }
                    else {
                        $elem.addClass('conversations');
                    }
                }
                else {
                    $elem.addClass('conversations');
                }
            }
            else {
                $elem.addClass('never');
            }
            if (time2last(ts)) {
                $elem.text(
                    time2last(ts)
                );
            }
            else {
                $elem.text(l[1051]);
            }
        };

        var _renderLastInteractionFail = noRender ? nop : function (r) {
            var $elem = $('.li_' + u_h);

            $elem
                .removeClass('never')
                .removeClass('cloud-drive')
                .removeClass('conversations')
                .removeClass('unread-conversations');


            $elem.addClass('never');
            $elem.text(l[1051]);
        };

        var $promise = new MegaPromise();

        $promise
            .done(_renderLastInteractionDone)
            .fail(_renderLastInteractionFail);

        if (_lastUserInteractionCacheInFlight[u_h] && _lastUserInteractionCacheInFlight[u_h] !== -1) {
            $promise.resolve(_lastUserInteractionCacheInFlight[u_h]);
        }
        else if (
            _lastUserInteractionPromiseCache[u_h] &&
            _lastUserInteractionPromiseCache[u_h].state() === 'pending'
        ) {
            return _lastUserInteractionPromiseCache[u_h];
        }
        else if (_lastUserInteractionCache[u_h] && _lastUserInteractionCacheInFlight[u_h] !== -1) {
            $promise.resolve(_lastUserInteractionCache[u_h]);
        }
        else if (
            (!_lastUserInteractionCache[u_h] || _lastUserInteractionCacheInFlight[u_h] === -1) &&
            (
                !_lastUserInteractionPromiseCache[u_h] ||
                _lastUserInteractionPromiseCache[u_h].state() !== 'pending'
            )
        ) {
            if (_lastUserInteractionCacheInFlight[u_h] === -1) {
                delete _lastUserInteractionCacheInFlight[u_h];
            }
            _lastUserInteractionPromiseCache[u_h] = mega.attr.getArrayAttribute(
                u_handle,
                'lstint',
                u_h,
                false,
                true
                )
                .always(function() {
                    _lastUserInteractionPromiseCache[u_h] = false;
                })
                .done(function (res) {
                    if (typeof res !== 'number') {
                        if (typeof res === 'undefined') {
                            // detected legacy value which was not unserialised properly....should re-initialise as
                            // empty value, e.g. no last interaction with that user (would be rebuilt by chat messages
                            // and stuff)
                            $promise.reject(false);
                        }
                        else {
                            if (!triggeredBySet) {
                                _lastUserInteractionCache[u_h] = res;
                            }
                            $promise.resolve(res);
                        }
                    }
                    else {
                        $promise.reject(false);
                        console.error("Failed to retrieve last interaction cache from attrib, response: ", res);
                    }
                })
                .fail(function(res) {
                    $promise.reject(res);
                });
        }
        else {
            throw new Error("This should not happen.");
        }

        return $promise;
    };

    /**
     * Set the last interaction for a contact (throttled internally)
     *
     * @param u_h {String} user handle
     * @param v {String} "$typeOfInteraction:$unixTimestamp" (see getLastInteractionWith for the types of int...)
     * @returns {Deferred}
     */
    var _realSetLastInteractionWith = function (u_h, v) {

        console.assert(u_handle, "missing u_handle, can't proceed");
        console.assert(u_h, "missing argument u_h, can't proceed");

        if (!u_handle || !u_h) {
            return MegaPromise.reject(EARGS);
        }

        var isDone = false;
        var $promise = createTimeoutPromise(
            () => {
                return isDone === true;
            },
            500,
            10000,
            false,
            `SetLastInteraction(${u_h})`
        );

        $promise.always(function () {
            isDone = true;
        });


        getLastInteractionWith(u_h, true)
            .done(function (timestamp) {
                if (_compareLastInteractionStamp(v, timestamp) === false) {
                    // older timestamp found in `v`, resolve the promise with the latest timestamp
                    $promise.resolve(v);
                    $promise.verify();
                }
                else {
                    _lastUserInteractionCache[u_h] = v;

                    $promise.resolve(_lastUserInteractionCache[u_h]);

                    // TODO: check why `M.u[u_h]` might not be set...
                    Object(M.u[u_h]).ts = parseInt(v.split(":")[1], 10);

                    $promise.verify();

                    mega.attr.setArrayAttribute(
                        'lstint',
                        u_h,
                        _lastUserInteractionCache[u_h],
                        false,
                        true
                    );
                }
            })
            .fail(function (res) {
                if (res === false || res === -9) {
                    if (res === -9 && _lastUserInteractionCache === false) {
                        _lastUserInteractionCache = {};
                    }
                    _lastUserInteractionCache[u_h] = v;
                    $promise.resolve(_lastUserInteractionCache[u_h]);

                    Object(M.u[u_h]).ts = parseInt(v.split(":")[1], 10);

                    mega.attr.setArrayAttribute(
                        'lstint',
                        u_h,
                        _lastUserInteractionCache[u_h],
                        false,
                        true
                    );

                    $promise.verify();
                }
                else {
                    $promise.reject(res);
                    console.error("setLastInteraction failed, err: ", res);
                    $promise.verify();
                }
            });

        return $promise;

    };

    /**
     * Internal method that flushes all queued setLastInteraction operations in one go.
     * Usually triggered by `setLastInteractionWith`
     *
     * @private
     */
    var _flushSetLastInteractionWith = function() {
        timerSetLastInteraction = null;

        for (var i = throttledSetLastInteractionOps.length - 1; i >= 0; i--) {
            var op = throttledSetLastInteractionOps[i];
            throttledSetLastInteractionOps.splice(i, 1);
            _lastUserInteractionCacheInFlight[op[0]] = -1;
            op[2].linkDoneAndFailTo(_realSetLastInteractionWith(op[0], op[1]));
        }
    };


    /**
     * Set the last interaction for a contact (throttled internally)
     *
     * @param u_h {String} user handle
     * @param v {String} "$typeOfInteraction:$unixTimestamp" (see getLastInteractionWith for the types of int...)
     * @returns {Deferred|MegaPromise}
     */
    var setLastInteractionWith = function(u_h, v) {
        var promise = new MegaPromise();

        // set on client side, to simulate a real commit
        var ts = Object(M.u[u_h]).ts;
        var newTs = parseInt(v.split(":")[1], 10);
        if (ts < newTs) {
            Object(M.u[u_h]).ts = newTs;
            _lastUserInteractionCacheInFlight[u_h] = v;
        }

        if (timerSetLastInteraction) {
            clearTimeout(timerSetLastInteraction);
        }
        timerSetLastInteraction = setTimeout(_flushSetLastInteractionWith, SET_LAST_INTERACTION_TIMER);

        for (var i = 0; i < throttledSetLastInteractionOps.length; i++) {
            var entry = throttledSetLastInteractionOps[i];
            var u_h2 = entry[0];
            var ts2 = parseInt(entry[1].split(":")[1], 10);

            if (u_h2 === u_h) {
                if (newTs < ts2) {
                    return MegaPromise.resolve(entry[1]);
                }
                else {
                    entry[1] = v;
                    return entry[2];
                }

            }
        }
        throttledSetLastInteractionOps.push([u_h, v, promise]);

        return promise;
    };

    exportScope.setLastInteractionWith = setLastInteractionWith;
    exportScope.getLastInteractionWith = getLastInteractionWith;
})(window);

/**
 * General common functionality for the new secure Registration and Login process including an
 * improved Password Processing Function (PPF) now with PBKDF2-HMAC-SHA512, a per user salt and 100,000 iterations.
 */
var security = {

    /** Minimum password length across the app for registration and password changes */
    minPasswordLength: 8,

    /**
     * Minimum password score across the app for registration and password changes. The score is calculated
     * using the score from the ZXCVBN library and the range is from 0 - 4 (very weak, weak, medium, good, strong)
     */
    minPasswordScore: 1,

    /** The number of iterations for the PPF (1-2 secs computation time) */
    numOfIterations: 100000,

    /** The length of the salt in bits */
    saltLengthInBits: 128,          // 16 Bytes

    /** The desired length of the derived key from the PPF in bits */
    derivedKeyLengthInBits: 256,    // 32 Bytes

    /**
     * Checks if the password is valid and meets minimum strength requirements
     * @param {String} password The user's password
     * @param {String} confirmPassword The second password the user typed again as a confirmation to avoid typos
     * @returns {true|String} Returns true if the password is valid, or the error message if not valid
     */
    isValidPassword: function(password, confirmPassword) {

        'use strict';
        // Check for a password
        if (!password) {
            return l.err_no_pass;   // Enter a password
        }
        // Check if the passwords are not the same
        if (password !== confirmPassword) {
            return l[9066];         // Passwords don't match. Check and try again.
        }

        // Check if there is whitespace at the start or end of the password
        if (password !== password.trim()) {
            return l[19855];        // Whitespace at the start or end of the password is not permitted.
        }

        // Check for minimum password length
        if (password.length < security.minPasswordLength) {
            return l[18701];        // Your password needs to be at least x characters long.
        }

        // Check that the estimator library is initialised
        if (typeof zxcvbn === 'undefined') {
            return l[1115] + ' ' + l[1116];     // The password strength verifier is still initializing.
        }                                       // Please try again in a few seconds.

        // Check for minimum password strength score from ZXCVBN library
        if ((zxcvbn(password).score < security.minPasswordScore)) {
            // Your password needs to be stronger.
            // Make it longer, add special characters or use uppercase and lowercase letters.
            return l[1104];
        }

        return true;
    },

    /**
     * Converts a UTF-8 string to a byte array
     * @param {String} string A string of any character including UTF-8 chars e.g. password123
     * @returns {Uint8Array} Returns a byte array
     */
    stringToByteArray: function(string) {

        'use strict';

        return new TextEncoder('utf-8').encode(string);
    },

    /**
     * A wrapper function to create the Client Random Value, Encrypted Master Key and Hashed Authentication Key.
     * These values are needed for when registering, changing the user's password, recovering with Master Key and
     * for parking the user's account.
     * @param {String} password The password from the user
     * @param {Array} masterKeyArray32 The unencrypted Master Key
     * @param {Function} completeCallback The function to be run after the keys are created which will pass the
     *                                    the clientRandomValueBytes, encryptedMasterKeyArray32,
     *                                    hashedAuthenticationKeyBytes
     *                                    and derivedAuthenticationKeyBytes as the parameters
     */
    deriveKeysFromPassword: function(password, masterKeyArray32, completeCallback) {

        'use strict';

        // Create the 128 bit (16 byte) Client Random Value and Salt
        var saltLengthInBytes = security.saltLengthInBits / 8;
        var clientRandomValueBytes = crypto.getRandomValues(new Uint8Array(saltLengthInBytes));
        var saltBytes = security.createSalt(clientRandomValueBytes);

        // Trim the password and convert it from ASCII/UTF-8 to a byte array
        var passwordTrimmed = $.trim(password);
        var passwordBytes = security.stringToByteArray(passwordTrimmed);

        // The number of iterations for the PPF and desired length in bits of the derived key
        var iterations = security.numOfIterations;
        var derivedKeyLength = security.derivedKeyLengthInBits;

        // Run the PPF
        security.deriveKey(saltBytes, passwordBytes, iterations, derivedKeyLength, function(derivedKeyBytes) {

            // Get the first 16 bytes as the Encryption Key and the next 16 bytes as the Authentication Key
            var derivedEncryptionKeyBytes = derivedKeyBytes.subarray(0, 16);
            var derivedAuthenticationKeyBytes = derivedKeyBytes.subarray(16, 32);

            // Get a hash of the Authentication Key which the API will use for authentication at login time
            var hashedAuthenticationKeyBytes = asmCrypto.SHA256.bytes(derivedAuthenticationKeyBytes);

            // Keep only the first 128 bits (16 bytes) of the Hashed Authentication Key
            hashedAuthenticationKeyBytes = hashedAuthenticationKeyBytes.subarray(0, 16);

            // Convert the Derived Encryption Key to a big endian array of 32 bytes, then encrypt the Master Key
            var derivedEncryptionKeyArray32 = base64_to_a32(ab_to_base64(derivedEncryptionKeyBytes));
            var cipherObject = new sjcl.cipher.aes(derivedEncryptionKeyArray32);
            var encryptedMasterKeyArray32 = encrypt_key(cipherObject, masterKeyArray32);

            // Pass the Client Random Value, Encrypted Master Key and Hashed Authentication Key to the calling function
            completeCallback(clientRandomValueBytes, encryptedMasterKeyArray32, hashedAuthenticationKeyBytes,
                derivedAuthenticationKeyBytes);
        });
    },

    /**
     * Creates a 128 bit Client Random Value and then derives the Salt from that.
     * The salt is created from SHA-256('mega.nz' || 'Padding' || Client Random Value)
     * @param {Uint8Array} clientRandomValueBytes The Client Random Value from which the salt will be constructed
     * @returns {Uint8Array} Returns the 256 bit (32 bytes) salt as a byte array
     */
    createSalt: function(clientRandomValueBytes) {

        'use strict';

        var saltString = 'mega.nz';
        var saltStringMaxLength = 200;  // 200 chars for 'mega.nz' + padding
        var saltHashInputLength = saltStringMaxLength + clientRandomValueBytes.length;  // 216 bytes

        // Pad the salt string to 200 chars with the letter P
        for (var i = saltString.length; i < saltStringMaxLength; i++) {
            saltString += 'P';
        }

        // Cronvert the salt to a byte array
        var saltStringBytes = security.stringToByteArray(saltString);

        // Concatenate the Client Random Value bytes to the end of the salt string bytes
        var saltInputBytesConcatenated = new Uint8Array(saltHashInputLength);
        saltInputBytesConcatenated.set(saltStringBytes);
        saltInputBytesConcatenated.set(clientRandomValueBytes, saltStringMaxLength);

        // Hash the bytes to create the salt
        var saltBytes = asmCrypto.SHA256.bytes(saltInputBytesConcatenated);

        // Return the salt which is needed for the PPF
        return saltBytes;
    },

    /*
     * Checks if Two-Factor Authentication is enabled for the user's account
     * @param {String} email The user's email address
     * @param {Function} completeCallback The function to run if successful
     */
    checkIfTwoFactorAuthEnabled: function(email, completeCallback) {

        'use strict';

        // Make Multi-Factor Auth Get request
        api_req({ a: 'mfag', e: email }, {
            callback: function(result) {

                // If enabled, send back true
                if (result === 1) {
                    completeCallback(true);
                }
                else {
                    completeCallback(false);
                }
            }
        });
    },

    /**
     * Fetch the user's salt from the API
     * @param {String} email The user's email address
     * @param {Function} completeCallback The function to run if successful
     */
    fetchAccountVersionAndSalt: function(email, completeCallback) {

        'use strict';

        // Send the email to the API
        api_req({ a: 'us0', user: email }, {
            callback: function(result) {

                // If successful
                if (typeof result === 'object') {

                    // Get the version and salt (there is no salt for old version 1 accounts)
                    var version = result.v;
                    var salt = (version >= 2) ? result.s : null;

                    // Run the callback
                    completeCallback(version, salt);
                }
                else {
                    // Show an error dialog
                    if (is_mobile) {
                        mobile.messageOverlay.show(l[47], l[200]);
                    }
                    else {
                        // Desktop error dialog
                        msgDialog('warningb', l[47], l[200]);
                    }

                    console.error('Error fetching user salt!', result);
                }
            }
        });
    },

    /**
     * A wrapper function used for deriving a key from a password
     * @param {Uint8Array} saltBytes The salt as a byte array
     * @param {String} passwordBytes The password as a byte array
     * @param {Number} iterations The cost factor / number of iterations of the PPF to perform
     * @param {Number} derivedKeyLength The length of the derived key to create
     * @param {Function} callback A function to call when the operation is complete
     */
    deriveKey: function(saltBytes, passwordBytes, iterations, derivedKeyLength, callback) {

        'use strict';

        // If Web Crypto method supported, use that as it's nearly as fast as native
        if (window.crypto && window.crypto.subtle && !is_microsoft) {
            security.deriveKeyWithWebCrypto(saltBytes, passwordBytes, iterations, derivedKeyLength, callback);
        }
        else {
            // Otherwise use asmCrypto which is the next fastest
            security.deriveKeyWithAsmCrypto(saltBytes, passwordBytes, iterations, derivedKeyLength, callback);
        }
    },

    /**
     * Derive the key using the Web Crypto API
     * @param {Uint8Array} saltBytes The salt as a byte array
     * @param {Uint8Array} passwordBytes The password as a byte array
     * @param {Number} iterations The cost factor / number of iterations of the PPF to perform
     * @param {Number} derivedKeyLength The length of the derived key to create
     * @param {Function} callback A function to call when the operation is complete
     */
    deriveKeyWithWebCrypto: function(saltBytes, passwordBytes, iterations, derivedKeyLength, callback) {

        'use strict';

        // Import the password as the key
        crypto.subtle.importKey(
            'raw', passwordBytes, 'PBKDF2', false, ['deriveBits']
        )
        .then(function(key) {

            // Required PBKDF2 parameters
            var params = {
                name: 'PBKDF2',
                hash: 'SHA-512',
                salt: saltBytes,
                iterations: iterations
            };

            // Derive bits using the algorithm
            return crypto.subtle.deriveBits(params, key, derivedKeyLength);
        })
        .then(function(derivedKeyArrayBuffer) {

            // Convert to a byte array
            var derivedKeyBytes = new Uint8Array(derivedKeyArrayBuffer);

            // Pass the derived key to the callback
            callback(derivedKeyBytes);
        });
    },

    /**
     * Derive the key using asmCrypto
     * @param {Uint8Array} saltBytes The salt as a byte array
     * @param {Uint8Array} passwordBytes The password as a byte array
     * @param {Number} iterations The cost factor / number of iterations of the PPF to perform
     * @param {Number} derivedKeyLength The length of the derived key to create
     * @param {Function} callback A function to call when the operation is complete
     */
    deriveKeyWithAsmCrypto: function(saltBytes, passwordBytes, iterations, derivedKeyLength, callback) {

        'use strict';

        // Convert the desired derived key length to bytes and derive the key
        var keyLengthBytes = derivedKeyLength / 8;
        var derivedKeyBytes = asmCrypto.PBKDF2_HMAC_SHA512.bytes(passwordBytes, saltBytes, iterations, keyLengthBytes);

        // Pass the derived key to the callback
        callback(derivedKeyBytes);
    },

    /**
     * A helper function for the check password feature (used when changing email or checking if they can remember).
     * This function will only pass the Derived Encryption Key to the completion callback.
     * @param {String} password The password from the user
     * @param {String} [saltBase64] Account Authentication Salt, if applicable
     * @returns {Promise} derivedEncryptionKeyArray32
     */
    getDerivedEncryptionKey: promisify(function(resolve, reject, password, saltBase64) {
        'use strict';

        saltBase64 = saltBase64 === undefined ? u_attr && u_attr.aas || '' : saltBase64;
        if (!saltBase64) {
            return resolve(prepare_key_pw(password));
        }

        // Convert the salt and password to byte arrays
        var saltArrayBuffer = base64_to_ab(saltBase64);
        var saltBytes = new Uint8Array(saltArrayBuffer);
        var passwordBytes = security.stringToByteArray(password);

        // The number of iterations for the PPF and desired length in bits of the derived key
        var iterations = security.numOfIterations;
        var derivedKeyLength = security.derivedKeyLengthInBits;

        // Run the PPF
        security.deriveKey(saltBytes, passwordBytes, iterations, derivedKeyLength, function(derivedKeyBytes) {

            // Get the first 16 bytes as the Encryption Key
            var derivedEncryptionKeyBytes = derivedKeyBytes.subarray(0, 16);

            // Convert the Derived Encryption Key to a big endian array of 32 bit values for decrypting the Master Key
            var derivedEncryptionKeyArray32 = base64_to_a32(ab_to_base64(derivedEncryptionKeyBytes));

            // Pass only the Derived Encryption Key back to the callback
            resolve(derivedEncryptionKeyArray32);
        });
    }),

    /**
     * Complete the Park Account process
     * @param {String} recoveryCode The recovery code from the email
     * @param {String} recoveryEmail The email address that is being recovered
     * @param {String} newPassword The new password for the account
     * @param {Function} completeCallback The function to run when the callback completes
     */
    resetUser: function(recoveryCode, recoveryEmail, newPassword, completeCallback) {

        'use strict';

        // Fetch the user's account version
        security.fetchAccountVersionAndSalt(recoveryEmail, function(version) {

            // If using the new registration method (v2)
            if (version === 2) {

                // Create fresh Master Key
                api_create_u_k();

                // Derive keys from the new password
                security.deriveKeysFromPassword(newPassword, u_k,
                    function(clientRandomValueBytes, encryptedMasterKeyArray32, hashedAuthenticationKeyBytes) {

                        // Convert Master Key, Hashed Authentication Key and Client Random Value to Base64
                        var encryptedMasterKeyBase64 = a32_to_base64(encryptedMasterKeyArray32);
                        var hashedAuthenticationKeyBase64 = ab_to_base64(hashedAuthenticationKeyBytes);
                        var clientRandomValueBase64 = ab_to_base64(clientRandomValueBytes);

                        // Create some random bytes to send to the API. This is not actually needed for the new v2 Park
                        // Account process. It was only used for old style registrations at confirmation time, which
                        // checked the password was correct locally by decrypting a known value and checking that
                        // value. ToDo: API team to remove need for the 'z' property in the 'erx' API request.
                        var ssc = Array(8);
                        for (var i = 8; i--;) {
                            ssc[i] = rand(0x100000000);
                        }
                        var sscString = a32_to_str(ssc);
                        var base64data = base64urlencode(sscString);

                        // Run API request to park the account and start a new one under the same email
                        api_req({
                            a: 'erx',
                            c: recoveryCode,
                            x: encryptedMasterKeyBase64,
                            y: {
                                crv: clientRandomValueBase64,
                                hak: hashedAuthenticationKeyBase64
                            },
                            z: base64data
                        },
                        { callback: completeCallback });
                    }
                );
            }
            else {
                // Otherwise use the old reset/park method
                api_resetuser({ callback: completeCallback }, recoveryCode, recoveryEmail, newPassword);
            }
        });
    },

    /**
     * Perform the Master Key re-encryption with a new password. If the password is passed to the function as null,
     * then the function will just check the validity of the recovery code. The next step in the flow is for the user
     * to change their password which will then call this function again with all the parameters.
     *
     * @param {String} recoveryCode The recovery code from the email
     * @param {String} masterKeyArray32 The Master/Recovery Key entered by the user
     * @param {String} recoveryEmail The email address that is being recovered
     * @param {String|null} newPassword The new password for the account (optional)
     * @param {Function} completeCallback The function to run when the callback completes
     */
    resetKey: function(recoveryCode, masterKeyArray32, recoveryEmail, newPassword, completeCallback) {

        'use strict';

        // Fetch the user's account version
        security.fetchAccountVersionAndSalt(recoveryEmail, function(version) {

            // If using the new registration method (v2)
            if (version === 2) {

                // Check the recovery code
                api_req({
                    a: 'erx',
                    r: 'gk',
                    c: recoveryCode
                },
                {
                    callback: function(result) {

                        // If the private RSA key was returned
                        if (typeof result === 'string') {

                            // If the decryption of the RSA private key failed, pass error back to callback
                            if (!verifyPrivateRsaKeyDecryption(result, masterKeyArray32)) {
                                completeCallback(EKEY);
                            }

                            // Complete the reset of the user's password using the Master Key provided
                            else if (newPassword) {
                                security.completeResetKey(newPassword, recoveryCode, masterKeyArray32,
                                                          completeCallback);
                            }
                            else {
                                completeCallback(0);
                            }
                        }
                        else {
                            completeCallback(result);
                        }
                    }
                });
            }
            else {
                // Otherwise use the old reset password by recovery key method
                api_resetkeykey(
                    { result: completeCallback }, recoveryCode, masterKeyArray32, recoveryEmail, newPassword
                );
            }
        });
    },

    /**
     * Complete the reset of the user's password using the Master Key provided by the user
     * @param {String} newPassword The new password for the account
     * @param {String} recoveryCode The recovery code from the email
     * @param {String} masterKeyArray32 The Master/Recovery Key entered by the user
     * @param {Function} completeCallback The function to run when the callback completes
     */
    completeResetKey: function(newPassword, recoveryCode, masterKeyArray32, completeCallback) {

        'use strict';

        // Derive keys from the new password
        security.deriveKeysFromPassword(newPassword, masterKeyArray32,
            function(clientRandomValueBytes, encryptedMasterKeyArray32, hashedAuthenticationKeyBytes) {

                // Convert Master Key, Hashed Authentication Key and Client Random Value to Base64
                var encryptedMasterKeyBase64 = a32_to_base64(encryptedMasterKeyArray32);
                var hashedAuthenticationKeyBase64 = ab_to_base64(hashedAuthenticationKeyBytes);
                var clientRandomValueBase64 = ab_to_base64(clientRandomValueBytes);

                // Run API request to park the account and start a new one under the same email
                api_req({
                    a: 'erx',
                    r: 'sk',
                    c: recoveryCode,
                    x: encryptedMasterKeyBase64,
                    y: {
                        crv: clientRandomValueBase64,
                        hak: hashedAuthenticationKeyBase64
                    }
                },
                { callback: completeCallback });
            }
        );
    },

    /**
     * Check whether the provided password is valid to decrypt a key.
     * @param {String|*} aPassword The password to test against.
     * @param {String|*} aMasterKey The encrypted master key.
     * @param {String|*} aPrivateKey The encrypted private key.
     * @param {String|*} [aSalt] Account authentication salt, if applicable.
     * @returns {Boolean|*} whether it succeed.
     */
    verifyPassword: promisify(function(resolve, reject, aPassword, aMasterKey, aPrivateKey, aSalt) {
        'use strict';

        if (typeof aPrivateKey === 'string') {
            aPrivateKey = base64_to_a32(aPrivateKey);
        }

        if (typeof aMasterKey === 'string') {
            aMasterKey = [[aMasterKey, aSalt]];
        }
        var keys = aMasterKey.concat();

        (function _next() {
            var pair = keys.pop();
            if (!pair) {
                return reject(ENOENT);
            }

            var mk = pair[0];
            var salt = pair[1];

            security.getDerivedEncryptionKey(aPassword, salt || false)
                .then(function(derivedKey) {
                    if (typeof mk === 'string') {
                        mk = base64_to_a32(mk);
                    }

                    var decryptedMasterKey = decrypt_key(new sjcl.cipher.aes(derivedKey), mk);
                    var decryptedPrivateKey = decrypt_key(new sjcl.cipher.aes(decryptedMasterKey), aPrivateKey);

                    if (crypto_decodeprivkey(a32_to_str(decryptedPrivateKey))) {
                        return resolve({k: decryptedMasterKey, s: salt});
                    }

                    onIdle(_next);
                })
                .catch(function(ex) {
                    console.warn(mk, salt, ex);
                    onIdle(_next);
                });
        })();
    }),

    /**
     * Complete the email verification process
     * @param {String} pwd The new password for the account.
     * @param {String} code The code from the email notification.
     * @returns {Promise}
     */
    completeVerifyEmail: promisify(function(resolve, reject, pwd, code) {
        'use strict';

        var req = {a: 'erx', c: code, r: 'v1'};
        var xhr = function(key, uh) {
            req.y = uh;
            req.x = a32_to_base64(key);
            M.req(req).then(resolve).catch(reject);
        };

        // If using the new registration method (v2)
        if (u_attr.aav > 1) {
            req.r = 'v2';

            security.deriveKeysFromPassword(pwd, u_k, tryCatch(function(crv, key, uh) {
                req.z = ab_to_base64(crv);
                xhr(key, ab_to_base64(uh));
            }, reject));
        }
        else {
            var aes = new sjcl.cipher.aes(prepare_key_pw(pwd));
            xhr(encrypt_key(aes, u_k), stringhash(u_attr.email.toLowerCase(), aes));
        }
    }),

    /**
     * Ask the user for email verification on account suspension.
     * @param {String} [aStep] What step of the email verification should be triggered.
     * @returns {undefined}
     */
    showVerifyEmailDialog: function(aStep) {
        'use strict';
        var name = 'verify-email' + (aStep ? '-' + aStep : '');

        if ($.hideTopMenu) {
            $.hideTopMenu();
        }

        // abort any ongoing dialog operation that may would get stuck by receiving an whyamiblocked=700
        M.safeShowDialog.abort();

        M.safeShowDialog(name, function() {
            parsepage(pages.placeholder);
            watchdog.registerOverrider('logout');

            var $dialog = $('.mega-dialog.' + name);
            if (!$dialog.length) {
                $('#loading').addClass('hidden');
                parsepage(pages['dialogs-common']);
                $dialog = $('.mega-dialog.' + name);
            }
            var showLoading = function() {
                loadingDialog.show();
                $dialog.addClass('arrange-to-back');
            };
            var hideLoading = function() {
                loadingDialog.hide();
                $dialog.removeClass('arrange-to-back');
            };
            var reset = function(step) {
                hideLoading();
                closeDialog();

                if (step === true) {
                    loadSubPage('login');
                }
                else {
                    security.showVerifyEmailDialog(step && step.to);
                }
            };
            $('.mega-dialog:visible').addClass('hidden');

            if (aStep === 'login-to-account') {
                var code = String(page).substr(11);

                onIdle(showLoading);
                console.assert(String(page).startsWith('emailverify'));

                M.req({a: 'erv', v: 2, c: code})
                    .always(function(res) {
                        loadingDialog.hide();
                        console.debug('erv', [res]);

                        if (res === EEXPIRED || res === ENOENT) {
                            return msgDialog('warninga', l[135], res === EEXPIRED ? l[7719] : l[22128], false, reset);
                        }
                        if (!Array.isArray(res) || !res[6]) {
                            return msgDialog('warninga', l[135], l[47], res < 0 ? api_strerror(res) : l[253], reset);
                        }

                        u_logout(true);
                        $dialog.removeClass('arrange-to-back');

                        u_handle = res[4];
                        u_attr = {u: u_handle, email: res[1], privk: res[6].privk, evc: code, evk: res[6].k};

                        if (is_mobile) {
                            $('button.js-close', $dialog).addClass('hidden');
                            $('.cancel-email-verify', $dialog).removeClass('hidden').rebind('click.cancel', function() {
                                loadSubPage("start");
                            });
                        }
                        else {
                            $('button.js-close', $dialog).removeClass('hidden').rebind('click.cancel', function() {
                                loadSubPage("start");
                            });
                            $('.cancel-email-verify', $dialog).addClass('hidden');
                        }

                        $('.mail', $dialog).val(u_attr.email);
                        $('button.next', $dialog).rebind('click.ve', function() {
                            var $input = $('.pass', $dialog);
                            var pwd = $input.val();

                            showLoading();
                            security.verifyPassword(pwd, u_attr.evk, u_attr.privk)
                                .then(function(res) {
                                    u_k = res.k;
                                    u_attr.aav = 1 + !!res.s;
                                    reset({to: 'set-new-pass'});
                                })
                                .catch(function(ex) {
                                    hideLoading();
                                    console.debug(ex);
                                    $input.megaInputsShowError(l[1102]).val('').focus();
                                });

                            return false;
                        });
                    });
            }
            else if (aStep === 'set-new-pass') {
                console.assert(u_attr && u_attr.evc, 'Invalid procedure...');

                $('button.finish', $dialog).rebind('click.ve', function() {
                    var pw1 = $('input.pw1', $dialog).val();
                    var pw2 = $('input.pw2', $dialog).val();

                    var error = function(msg) {
                        hideLoading();
                        $('input', $dialog)
                            .val('').trigger('blur')
                            .first().trigger('input').megaInputsShowError(msg).trigger('focus');
                        return false;
                    };

                    var pwres = security.isValidPassword(pw1, pw2);
                    if (pwres !== true) {
                        return error(pwres);
                    }

                    showLoading();
                    security.verifyPassword(pw1, u_attr.evk, u_attr.privk)
                        .then(function() {
                            // Do not allow to use a old known password
                            error(l[22675]);
                        })
                        .catch(function(ex) {
                            if (ex !== ENOENT) {
                                console.error(ex);
                                return error(l[8982]);
                            }

                            security.completeVerifyEmail(pw1, u_attr.evc)
                                .then(function() {
                                    login_email = u_attr.email;
                                    watchdog.unregisterOverrider('logout');

                                    u_logout(true);
                                    eventlog(99728);
                                    loadSubPage('login');
                                })
                                .catch(function(ex) {
                                    hideLoading();
                                    msgDialog('warninga',
                                              l[135],
                                              l[47],
                                              ex < 0 ? api_strerror(ex) : ex,
                                              reset.bind(null, false));
                                });
                        });

                    return false;
                });
            }
            else {
                $('.send-email', $dialog).rebind('click.ve', function() {
                    $(this).unbind('click.ve').addClass('disabled');
                    M.req('era').always(function(res) {
                        $('aside.status', $dialog).addClass('hidden');
                        const contactPage = () => {
                            mega.redirect('mega.io', 'contact', false, false, false);
                        };

                        if (res === 0) {
                            $('aside.status', $dialog).removeClass('hidden');
                        }
                        else if (res === ETEMPUNAVAIL) {
                            msgDialog('warninga', l[135], l[23628], l[23629], contactPage);
                        }
                        else {
                            msgDialog('warninga', l[135], l[47], api_strerror(res), contactPage);
                        }
                    });
                    return false;
                });
            }

            var $inputs = $('input', $dialog);
            $inputs.rebind('keypress.ve', function(ev) {
                var key = ev.code || ev.key;

                if (key === 'Enter') {
                    if ($inputs.get(0) === this) {
                        $inputs.trigger('blur');
                        $($inputs.get(1)).trigger('focus');
                    }
                    else {
                        $('button.next, button.finish, button.send-email', $dialog).trigger('click');
                    }
                }
            });

            mega.ui.MegaInputs($inputs);
            return $dialog;
        });
    }
};


/**
 * Registration specific functionality for the new secure Registration process
 */
security.register = {

    /** Backup of the details to re-send the email if requested  */
    sendEmailRequestParams: false,

    /**
     * Create new account registration
     * @param {String} firstName The user's first name
     * @param {String} lastName The user's last name
     * @param {String} email The user's email address
     * @param {String} password The user's password
     * @param {Boolean} fromProPage Whether the registration started on the Pro page or not
     * @param {Function} completeCallback A function to run when the registration is complete
     */
    startRegistration: function(firstName, lastName, email, password, fromProPage, completeCallback) {
        'use strict';

        if (this.sendEmailRequestParams) {
            console.error('startRegistration blocked, ongoing.');
            return;
        }
        this.sendEmailRequestParams = true;

        // Show loading dialog
        loadingDialog.show();

        // First create an ephemeral account and the Master Key (to be removed at a later date)
        security.register.createEphemeralAccount(function() {

            // Derive the Client Random Value, Encrypted Master Key and Hashed Authentication Key
            security.deriveKeysFromPassword(password, u_k,
                function(clientRandomValueBytes, encryptedMasterKeyArray32, hashedAuthenticationKeyBytes) {

                    // Encode parameters to Base64 before sending to the API
                    var sendEmailRequestParams = {
                        a: 'uc2',
                        n: base64urlencode(to8(firstName + ' ' + lastName)),         // Name (used just for the email)
                        m: base64urlencode(email),                                   // Email
                        crv: ab_to_base64(clientRandomValueBytes),                   // Client Random Value
                        k: a32_to_base64(encryptedMasterKeyArray32),                 // Encrypted Master Key
                        hak: ab_to_base64(hashedAuthenticationKeyBytes),             // Hashed Authentication Key
                        v: 2                                                         // Version of this protocol
                    };

                    // If this was a registration from the Pro page
                    if (fromProPage === true) {
                        sendEmailRequestParams.p = 1;
                    }

                    // Send signup link email
                    security.register.sendSignupLink(sendEmailRequestParams, firstName, lastName, email,
                                                     completeCallback);
                }
            );
        });
    },

    /**
     * Create an ephemeral account
     * @param {Function} callbackFunction The callback function to run once the ephemeral account is created
     */
    createEphemeralAccount: function(callbackFunction) {

        'use strict';

        // Set a flag to check at the end of the registration process
        if (is_mobile) {
            localStorage.signUpStartedInMobileWeb = '1';
        }

        // If there is no ephemeral account already
        if (u_type === false) {

            // Initialise local storage
            u_storage = init_storage(localStorage);

            // Create anonymous ephemeral account
            u_checklogin({
                checkloginresult: function(context, userType) {

                    // Set the user type
                    u_type = userType;

                    // Continue registering the account
                    callbackFunction();
                }
            }, true);
        }

        // If they already have an ephemeral account
        else if (u_type === 0) {

            // Continue registering the account
            callbackFunction();
        }
    },

    /**
     * Start the registration process and send a signup link to the user
     * @param {Object} sendEmailRequestParams An object containing the data to send to the API.
     * @param {String} firstName The user's first name
     * @param {String} lastName The user's last name
     * @param {String} email The user's email
     * @param {Function} completeCallback A function to run when the registration is complete
     */
    sendSignupLink: function(sendEmailRequestParams, firstName, lastName, email, completeCallback) {

        'use strict';

        // Save the input variables so they can be re-used to resend the details with a different email
        security.register.sendEmailRequestParams = {
            firstName: firstName,
            lastName: lastName
        };

        // Run the API request
        api_req(sendEmailRequestParams, {
            callback: function(result) {

                // Hide the loading spinner
                loadingDialog.hide();

                // If successful result, send additional information to the API for the name
                if (result === 0) {
                    security.register.sendAdditionalInformation(firstName, lastName);
                }
                security.register.sendEmailRequestParams = false;

                // Run the callback requested by the calling function to show a check email dialog or show error
                completeCallback(result, firstName, lastName, email);
            }
        });
    },

    /**
     * Cache registration data like name, email etc in case they refresh the page and need to resend the email
     * @param {Object} registerData An object containing keys 'first', 'last', 'name', 'email' and optional 'password'
     *                              for old style registrations.
     */
    cacheRegistrationData: function(registerData) {

        'use strict';

        // Remove password from the object so it doesn't get saved to
        // localStorage for the resend process.

        delete registerData.password;

        localStorage.awaitingConfirmationAccount = JSON.stringify(registerData);

        if (localStorage.voucher) {
            const data = [localStorage.voucher, localStorage[localStorage.voucher] || -1];
            mega.attr.set('promocode', JSON.stringify(data), -2, true).dump();
        }
    },

    /**
     * Repeat the registration process and send a signup link to the user via the new email address they entered
     * @param {String} firstName The user's first name
     * @param {String} lastName The user's last name
     * @param {String} newEmail The user's corrected email
     * @param {Function} completeCallback A function to run when the registration is complete
     */
    repeatSendSignupLink: function(firstName, lastName, newEmail, completeCallback) {

        'use strict';

        // Re-encode the parameters to Base64 before sending to the API
        var sendEmailRequestParams = {
            a: 'uc2',
            n: base64urlencode(to8(firstName + ' ' + lastName)),  // Name (used just for the email)
            m: base64urlencode(newEmail)                          // Email
        };

        // Run the API request
        api_req(sendEmailRequestParams, {
            callback: function(result) {

                // Hide the loading spinner
                loadingDialog.hide();

                // If successful result, show a dialog success
                if (is_mobile && result === 0) {
                    mobile.messageOverlay.show(l[16351]);    // The email was sent successfully.
                }

                // Run the callback requested by the calling function to show a check email dialog or whatever
                completeCallback(result, firstName, lastName, newEmail);
            }
        });
    },

    /**
     * Sends additional information e.g. first name and last name to the API
     * @param {String} firstName The user's first name
     * @param {String} lastName The user's last name
     */
    sendAdditionalInformation: function(firstName, lastName) {

        'use strict';

        // Set API request options
        var options = {
            a: 'up',
            terms: 'Mq',
            firstname: base64urlencode(to8(firstName)),
            lastname: base64urlencode(to8(lastName)),
            name2: base64urlencode(to8(firstName + ' ' + lastName))
        };

        if (mega.affid) {
            options.aff = mega.affid;
        }

        // Send API request
        api_req(options);
    },

    /**
     * Verifies the email confirmation code after registering
     * @param {String} confirmCode The confirm code from the registration email
     * @param {Function} completeCallback The function to run when the email confirm code has been sent to the API
     */
    verifyEmailConfirmCode: function(confirmCode, completeCallback) {

        'use strict';

        // Send the confirmation code back to the API
        api_req({ a: 'ud2', c: confirmCode }, {
            callback: function(result) {

                // If successful
                if (typeof result === 'object') {

                    // Decode the result (array index 0 = email, 1 = name, 2 = Base64 encoded user handle)
                    var email = base64urldecode(result[0]);

                    // Pass the email back
                    completeCallback(result, email);
                }
                else {
                    // Pass the error code back
                    completeCallback(result);
                }
            }
        });
    }
};


/**
 * Login specific functionality for the new secure Login process
 */
security.login = {

    /** Cache of the login email in case we need to resend after they have entered their two factor code */
    email: null,

    /** Cache of the login password in case we need to resend after they have entered their two factor code */
    password: null,

    /** Cache of the flag to remember that the user wants to remain logged in after they close the browser */
    rememberMe: false,

    /** Callback to run after login is complete */
    loginCompleteCallback: null,


    /**
     * Check which login method the user is using (either the old process or the new process)
     * ToDo: Add check to the email field on the login page if it is prefilled or they finish typing to fetch the salt
     * @param {String} email The user's email addresss
     * @param {String} password The user's password as entered
     * @param {String|null} pinCode The two-factor authentication PIN code (6 digit number), or null if not applicable
     * @param {Boolean} rememberMe A boolean for if they checked the Remember Me checkbox on the login screen
     * @param {Function} oldStartCallback A callback for starting the old login process
     * @param {Function} newStartCallback A callback for starting the new login process
     */
    checkLoginMethod: function(email, password, pinCode, rememberMe, oldStartCallback, newStartCallback) {
        'use strict';

        console.assert(!!password, 'checkLoginMethod: blocked, pwd missing.');
        console.assert(pinCode || !security.login.email, 'checkLoginMethod: blocked, ongoing.');
        if ((!pinCode && security.login.email) || !password) {
            return;
        }

        // Temporarily cache the email, password and remember me checkbox status
        // in case we need to resend after they have entered their two factor code
        security.login.email = email;
        security.login.password = password;
        security.login.rememberMe = rememberMe;

        // Fetch the user's salt from the API
        security.fetchAccountVersionAndSalt(email, function(version, salt) {

            // If using the new method pass through the salt as well
            if (version === 2) {
                newStartCallback(email, password, pinCode, rememberMe, salt);
            }

            // Otherwise using the old method
            else if (version === 1) {
                oldStartCallback(email, password, pinCode, rememberMe);
            }
        });
    },

    /**
     * Start the login using the new process
     * @param {String} email The user's email addresss
     * @param {String} password The user's password as entered
     * @param {String|null} pinCode The two-factor authentication PIN code (6 digit number), or null if not applicable
     * @param {Boolean} rememberMe A boolean for if they checked the Remember Me checkbox on the login screen
     * @param {String} salt The user's salt as a Base64 URL encoded string
     * @param {Function} loginCompleteCallback The final callback once the login is complete
     */
    startLogin: function(email, password, pinCode, rememberMe, salt, loginCompleteCallback) {

        'use strict';

        // Convert the salt and password to byte arrays
        var saltArrayBuffer = base64_to_ab(salt);
        var saltBytes = new Uint8Array(saltArrayBuffer);
        var passwordBytes = security.stringToByteArray(password);

        // The number of iterations for the PPF and desired length in bits of the derived key
        var iterations = security.numOfIterations;
        var derivedKeyLength = security.derivedKeyLengthInBits;

        // Set the callback to run after login is complete
        security.login.loginCompleteCallback = loginCompleteCallback;

        // Run the PPF
        security.deriveKey(saltBytes, passwordBytes, iterations, derivedKeyLength, function(derivedKeyBytes) {

            // Get the first 16 bytes as the Encryption Key and the next 16 bytes as the Authentication Key
            var derivedEncryptionKeyBytes = derivedKeyBytes.subarray(0, 16);
            var derivedAuthenticationKeyBytes = derivedKeyBytes.subarray(16, 32);
            var authenticationKeyBase64 = ab_to_base64(derivedAuthenticationKeyBytes);

            // Convert the Derived Encryption Key to a big endian array of 32 bit values for decrypting the Master Key
            var derivedEncryptionKeyArray32 = base64_to_a32(ab_to_base64(derivedEncryptionKeyBytes));

            // Authenticate with the API
            security.login.sendAuthenticationKey(email, pinCode, authenticationKeyBase64, derivedEncryptionKeyArray32);
        });
    },

    /**
     * Authenticate with the API by sending the Authentication Key
     * @param {String} email The user's email address
     * @param {String|null} pinCode The two-factor authentication PIN code (6 digit number), or null if not applicable
     * @param {String} authenticationKeyBase64 The 128 bit Authentication Key encdoded as URL encoded Base64
     * @param {Array} derivedEncryptionKeyArray32 A 128 bit key encoded as a big endian array of 32 bit values which
     *                                            was used to encrypt the Master Key
     */
    sendAuthenticationKey: function(email, pinCode, authenticationKeyBase64, derivedEncryptionKeyArray32) {

        'use strict';

        // Check for too many login attempts
        if (api_getsid.etoomany + 3600000 > Date.now() || location.host === 'webcache.googleusercontent.com') {
            security.login.loginCompleteCallback(ETOOMANY);
            return false;
        }

        // Setup the login request
        var requestVars = { a: 'us', user: email, uh: authenticationKeyBase64 };

        // If the two-factor authentication code was entered by the user, add it to the request as well
        if (pinCode !== null) {
            requestVars.mfa = pinCode;
        }

        // Send the Email and Authentication Key to the API
        api_req(requestVars, {
            callback: function(result) {

                // If successful
                if (typeof result === 'object') {

                    // Get values from Object
                    var temporarySessionIdBase64 = result.tsid;
                    var encryptedSessionIdBase64 = result.csid;
                    var encryptedMasterKeyBase64 = result.k;
                    var encryptedPrivateRsaKey = result.privk;
                    var userHandle = result.u;

                    // Decrypt the Master Key
                    var encryptedMasterKeyArray32 = base64_to_a32(encryptedMasterKeyBase64);
                    var cipherObject = new sjcl.cipher.aes(derivedEncryptionKeyArray32);
                    var decryptedMasterKeyArray32 = decrypt_key(cipherObject, encryptedMasterKeyArray32);

                    // If the temporary session ID is set then we need to generate RSA keys
                    if (typeof temporarySessionIdBase64 !== 'undefined') {
                        security.login.skipToGenerateRsaKeys(decryptedMasterKeyArray32, temporarySessionIdBase64);
                    }
                    else {
                        // Otherwise continue a regular login
                        security.login.decryptRsaKeyAndSessionId(decryptedMasterKeyArray32, encryptedSessionIdBase64,
                                                                 encryptedPrivateRsaKey, userHandle);
                    }
                }
                else {
                    // Return failure
                    security.login.loginCompleteCallback(result);
                }
            }
        });
    },

    /**
     * Sets some session variables and skips to the RSA Key Generation page and process
     * @param {Array} masterKeyArray32 The unencrypted Master Key
     * @param {String} temporarySessionIdBase64 The temporary session ID send from the API
     */
    skipToGenerateRsaKeys: function(masterKeyArray32, temporarySessionIdBase64) {

        'use strict';

        // Set global values which are used everywhere
        u_k = masterKeyArray32;
        u_sid = temporarySessionIdBase64;
        u_k_aes = new sjcl.cipher.aes(masterKeyArray32);

        // Set the Session ID for future API requests
        api_setsid(temporarySessionIdBase64);

        // Set to localStorage as well
        u_storage.k = JSON.stringify(masterKeyArray32);
        u_storage.sid = temporarySessionIdBase64;

        // Redirect to key generation page
        loadSubPage('key');
    },

    /**
     * Decrypts the RSA private key and the RSA encrypted session ID
     * @param {Array} masterKeyArray32 The unencrypted Master Key
     * @param {String} encryptedSessionIdBase64 The encrypted session ID as a Base64 string
     * @param {String} encryptedPrivateRsaKeyBase64 The private RSA key as a Base64 string
     * @param {String} userHandle The encrypted user handle from the 'us' response
     */
    decryptRsaKeyAndSessionId: function(masterKeyArray32, encryptedSessionIdBase64,
                                        encryptedPrivateRsaKeyBase64, userHandle) {

        'use strict';

        const errobj = {};
        var keyAndSessionData = false;

        try {

            if (typeof userHandle !== 'string' || userHandle.length !== 11) {
                eventlog(99752, JSON.stringify([1, 11, userHandle]));

                console.error("Incorrect user handle in the 'us' response", userHandle);

                Soon(() => {
                    msgDialog('warninga', l[135], l[8853], userHandle);
                });

                return false;
            }

            // Decrypt and decode the RSA Private Key
            var cipherObject = new sjcl.cipher.aes(masterKeyArray32);
            var encryptedPrivateRsaKeyArray32 = base64_to_a32(encryptedPrivateRsaKeyBase64);
            var decryptedPrivateRsaKey = decrypt_key(cipherObject, encryptedPrivateRsaKeyArray32);
            var decryptedPrivateRsaKeyBigEndianString = a32_to_str(decryptedPrivateRsaKey);

            const decodedPrivateRsaKey = crypto_decodeprivkey(decryptedPrivateRsaKeyBigEndianString, errobj);
            if (!decodedPrivateRsaKey) {
                console.error('RSA key decoding failed (%o)..', errobj);

                eventlog(99752, JSON.stringify([1, 10, errobj]));

                Soon(() => {
                    msgDialog('warninga', l[135], l[8853], JSON.stringify(errobj));
                });

                return false;
            }

            // Decrypt the Session ID using the RSA Private Key
            var encryptedSessionIdBytes = base64urldecode(encryptedSessionIdBase64);
            var decryptedSessionId = crypto_rsadecrypt(encryptedSessionIdBytes, decodedPrivateRsaKey);
            var decryptedSessionIdSubstring = decryptedSessionId.substr(0, 43);
            var decryptedSessionIdBase64 = base64urlencode(decryptedSessionIdSubstring);

            // Get the user handle from the decrypted Session ID (11 bytes starting at offset 16 bytes)
            const sessionIdUserHandle = decryptedSessionId.substring(16, 27);

            // Add a check that the decrypted sid and res.u aren't shorter than usual before making the comparison.
            // Otherwise, we could construct an oracle based on shortened csids with single-byte user handles.
            if (decryptedSessionId.length !== 255) {
                eventlog(99752, JSON.stringify([1, 13, userHandle, decryptedSessionId.length]));

                throw new Error(`Incorrect length of Session ID ${decryptedSessionId.length}`);
            }

            // Check that the user handle included in the Session ID matches the one sent in the 'us' response
            if (sessionIdUserHandle !== userHandle) {
                eventlog(99752, JSON.stringify([1, 14, userHandle]));

                throw new Error(`User handle mismatch! us-req:"${userHandle}" != session:"${sessionIdUserHandle}"`);
            }

            // Set the data
            keyAndSessionData = [masterKeyArray32, decryptedSessionIdBase64, decodedPrivateRsaKey];
        }
        catch (ex) {
            if (!eventlog.sent['99752']) {
                eventlog(99752, JSON.stringify([1, 12, userHandle, errobj, String(ex).split('\n')[0]]));
            }

            console.error('Error decrypting or decoding the private RSA key or Session ID!', ex);

            // Show an error dialog
            Soon(() => {
                msgDialog('warninga', l[135], l[8853], `${ex}`);
            });

            return false;
        }

        // Continue with the flow
        security.login.setSessionVariables(keyAndSessionData);
    },

    /**
     * Set the session variables and complete the login
     * @param {Array} keyAndSessionData A basic array consisting of:
     *     [
     *        {Array} The unencrypted Master Key,
     *        {String} The decrypted Session ID as a Base64 string,
     *        {Array} The decoded RSA Private Key as an array of parts
     *     ]
     */
    setSessionVariables: function(keyAndSessionData) {
        'use strict';

        // Check if the Private Key and Session ID were decrypted successfully
        if (keyAndSessionData === false) {
            security.login.loginCompleteCallback(false);
            return false;
        }

        // Set variables
        var masterKeyArray32 = keyAndSessionData[0];
        var decryptedSessionIdBase64 = keyAndSessionData[1];
        var decodedPrivateRsaKey = keyAndSessionData[2];

        // Set flag
        localStorage.wasloggedin = true;

        // Remove all previous login data
        u_logout();

        // Use localStorage if the user checked the Remember Me checkbox, otherwise use temporary sessionStorage
        u_storage = init_storage(security.login.rememberMe ? localStorage : sessionStorage);

        // Store the Master Key and Session ID
        u_storage.k = JSON.stringify(masterKeyArray32);
        u_storage.sid = decryptedSessionIdBase64;

        // Notify other tabs of login
        watchdog.notify('login', [!security.login.rememberMe && masterKeyArray32, decryptedSessionIdBase64]);

        // Store the RSA private key
        if (decodedPrivateRsaKey) {
            u_storage.privk = base64urlencode(crypto_encodeprivkey(decodedPrivateRsaKey));
        }

        // Cleanup temporary login variables
        security.login.email = null;
        security.login.password = null;
        security.login.rememberMe = false;

        // Continue to perform 'ug' request and afterwards run the loginComplete callback
        u_checklogin4(u_storage.sid)
            .then((res) => {
                security.login.loginCompleteCallback(res);

                // Logging to see how many people are signing in
                eventlog(is_mobile ? 99629 : 99630);

                // Broadcast login event
                mBroadcaster.sendMessage('login', keyAndSessionData);

                return res;
            })
            .dump('sec.login');
    },

    /**
     * Handles common errors like Two-Factor PIN issues, suspended accounts,
     * too many login attempts and incomplete registration
     * @param {Number} result A negative number if there was an error, or positive if login was successful
     * @param {type} oldStartLoginCallback
     * @param {type} newStartLoginCallback
     * @returns {Boolean} Returns true if the error was handled by this function, otherwise false and it will continue
     */
    checkForCommonErrors: function(result, oldStartLoginCallback, newStartLoginCallback) {
        'use strict';

        loadingDialog.hide();

        // Reset the 2FA dialog back to default UI
        twofactor.loginDialog.resetState();

        // If the Two-Factor Auth PIN is required
        if (result === EMFAREQUIRED) {

            // Request the 2FA PIN by showing the dialog, then after that it will re-run this function
            twofactor.loginDialog.init(oldStartLoginCallback, newStartLoginCallback);
            return true;
        }

        // If there was a 2FA error, show a message that the PIN code was incorrect and clear the text field
        else if (result === EFAILED) {
            twofactor.loginDialog.showVerificationError();
            return true;
        }

        // Cleanup temporary login variables
        security.login.email = null;
        security.login.password = null;
        security.login.rememberMe = false;

        // close two-factor dialog if it was opened
        twofactor.loginDialog.closeDialog();

        // Check for suspended account
        if (result === EBLOCKED) {
            msgDialog('warninga', l[6789], l[730]);
            return true;
        }

        // Check for too many login attempts
        else if (result === ETOOMANY) {
            api_getsid.etoomany = Date.now();
            api_getsid.warning();
            return true;
        }

        // Check for incomplete registration
        else if (result === EINCOMPLETE) {
            if (is_mobile) {
                mobile.messageOverlay.show(l[882], l[9082]);
            }
            else {
                msgDialog('warningb', l[882], l[9082]); // This account has not completed the registration
            }

            return true;
        }

        // Not applicable to this function
        return false;
    },

    /**
     * Silently upgrades a user's account from version 1 to the improved version 2 format (which has a per user salt)
     * and using the user's existing password. This is a general security improvement to upgrade all legacy users to
     * the latest account format when they log in.
     * @param {Number} loginResult The result from the v1 postLogin function which returns from u_checklogin3a
     * @param {Array} masterKey The unencrypted Master Key as an array of Int32 values
     * @param {String} password The current password from the user entered at login
     * @param {Function} completeLoginCallback The function to continue the login success (or failure) flow
     * @returns {void}
     */
    checkToUpgradeAccountVersion: function(loginResult, masterKey, password, completeLoginCallback) {

        'use strict';

        // Double check that: 1) not currently in registration signup, 2) account version is v1 and 3) login succeeded
        if (!confirmok && typeof u_attr === 'object' && u_attr.aav === 1 && loginResult !== false && loginResult >= 0) {

            // Show a console message in case it will take a while
            if (d) {
                console.info('Attempting to perform account version upgrade to v2...');
            }

            // Create the Client Random Value, re-encrypt the Master Key and create the Hashed Authentication Key
            security.deriveKeysFromPassword(password, masterKey, (crvBytes, encryptedMasterKey, hakBytes) => {

                // Convert to Base64
                const encryptedMasterKeyBase64 = a32_to_base64(encryptedMasterKey);
                const hashedAuthenticationKeyBase64 = ab_to_base64(hakBytes);
                const clientRandomValueBase64 = ab_to_base64(crvBytes);
                const saltBase64 = ab_to_base64(security.createSalt(crvBytes));

                // Prepare the Account Version Upgrade (avu) request
                const requestParams = {
                    a: 'avu',
                    emk: encryptedMasterKeyBase64,
                    hak: hashedAuthenticationKeyBase64,
                    crv: clientRandomValueBase64
                };

                // Send API request to change password
                api_req(requestParams, {
                    callback: (result) => {

                        // If successful
                        if (result === 0) {

                            // Update global user attributes (key, salt and version) because the
                            // 'ug' request is not re-done, nor are action packets sent for this
                            u_attr.k = encryptedMasterKeyBase64;
                            u_attr.aas = saltBase64;
                            u_attr.aav = 2;

                            // Log to console
                            if (d) {
                                console.info('Account version upgrade to v2 successful.');
                            }

                            // Log to Stats (to know how many are successfully upgraded)
                            eventlog(99770, true);
                        }
                        else {
                            // Log failures as well (to alert us of bugs)
                            eventlog(99771, true);
                        }

                        // If not successful, it will attempt again on next login, continue to update UI
                        completeLoginCallback(result);
                    }
                });
            });
        }
        else {
            // Otherwise continue to the rest of the login flow (including error handling) without upgrading
            completeLoginCallback();
        }
    }
};


/**
 * Common functionality for desktop/mobile webclient for changing the password using the old and new processes
 */
security.changePassword = {

    /**
     * Change the user's password using the old method
     * @param {String} newPassword The new password
     * @param {String|null} twoFactorPin The 2FA PIN code or null if not applicable
     * @param {Function} completeCallback The function to run when complete (to update the UI)
     */
    oldMethod: function(newPassword, twoFactorPin, completeCallback) {

        'use strict';

        // Otherwise change the password using the old method
        const newPasswordTrimmed = $.trim(newPassword);
        const pw_aes = new sjcl.cipher.aes(prepare_key_pw(newPasswordTrimmed));
        const encryptedMasterKeyBase64 = a32_to_base64(encrypt_key(pw_aes, u_k));
        const userHash = stringhash(u_attr.email.toLowerCase(), pw_aes);

        // Prepare the request
        var requestParams = {
            a: 'up',
            k: encryptedMasterKeyBase64,
            uh: userHash
        };

        // If the 2FA PIN was entered, send it with the request
        if (twoFactorPin !== null) {
            requestParams.mfa = twoFactorPin;
        }

        // Make API request to change the password
        api_req(requestParams, {
            callback: function(result) {

                // If successful, update user attribute key property with the Encrypted Master Key
                if (result) {
                    u_attr.k = encryptedMasterKeyBase64;
                }

                // Update UI
                completeCallback(result);
            }
        });
    },

    /**
     * Change the user's password using the new method
     * @param {String} newPassword The new password
     * @param {String|null} twoFactorPin The 2FA PIN code or null if not applicable
     * @param {Function} completeCallback The function to run when complete (to update the UI)
     */
    newMethod: function(newPassword, twoFactorPin, completeCallback) {

        'use strict';

        // Create the Client Random Value, Encrypted Master Key and Hashed Authentication Key
        security.deriveKeysFromPassword(newPassword, u_k,
            function(clientRandomValueBytes, encryptedMasterKeyArray32, hashedAuthenticationKeyBytes) {

                // Convert to Base64
                var encryptedMasterKeyBase64 = a32_to_base64(encryptedMasterKeyArray32);
                var hashedAuthenticationKeyBase64 = ab_to_base64(hashedAuthenticationKeyBytes);
                var clientRandomValueBase64 = ab_to_base64(clientRandomValueBytes);
                var saltBase64 = ab_to_base64(security.createSalt(clientRandomValueBytes));

                // Prepare the request
                var requestParams = {
                    a: 'up',
                    k: encryptedMasterKeyBase64,
                    uh: hashedAuthenticationKeyBase64,
                    crv: clientRandomValueBase64
                };

                // If the 2FA PIN was entered, send it with the request
                if (twoFactorPin !== null) {
                    requestParams.mfa = twoFactorPin;
                }

                // Send API request to change password
                api_req(requestParams, {
                    callback: function(result) {

                        // If successful, update global user attributes key and salt as the 'ug' request is not re-done
                        if (result) {
                            u_attr.k = encryptedMasterKeyBase64;
                            u_attr.aas = saltBase64;
                        }

                        // Update UI
                        completeCallback(result);
                    }
                });
            }
        );
    },

    /**
     * Checks for the user's current Account Authentication Version (e.g. v1 or v2)
     * @param {Function} callbackFunction The function to run when the version has been determined
     * @returns {void}
     */
    checkAccountVersion: function(callbackFunction) {

        'use strict';

        // If the Account Authentication Version is already v2 we don't need to check the version again via the API
        if (u_attr && u_attr.aav === 2) {
            callbackFunction(u_attr.aav);
        }
        else {
            // If we are currently a v1 account, we must check here for the current account version (in case it changed
            // recently in another app/browser) because we don't want the other app/browser to have logged in and
            // updated to v2 and then the current account which is still logged in here as v1 to overwrite that with
            // the old format data when they change password. If that happened the user would not be able to log into
            // their account anymore.
            M.req({ a: 'ug' })
                .then((res) => {

                    // If successful, pass the Account Authentication Version (int) to the callback function
                    callbackFunction(res.aav);
                })
                .catch((ex) => {

                    loadingDialog.hide();

                    // Oops, something went wrong
                    msgDialog('warninga', l[135], l[47], ex < 0 ? api_strerror(ex) : ex);
                });
        }
    },

    isPasswordTheSame: function(newPassword, method) {
        "use strict";
        var operation = new MegaPromise();
        // registration v2
        if (method === 2) {
            if (!u_attr || typeof u_attr.aas === "undefined" || typeof u_attr.k === "undefined" || !u_k) {
                return operation.reject(0);
            }

            var saltBytes = base64_to_ab(u_attr.aas);
            var passwordBytes = security.stringToByteArray($.trim(newPassword));

            security.deriveKey(saltBytes, passwordBytes, security.numOfIterations,
                security.derivedKeyLengthInBits, function(derivedKeyBytes) {

                    // callback could be Promise based or not
                    var derivedEncryptionKeyBytes = derivedKeyBytes.subarray(0, 16);
                    var derivedEncryptionKeyArray32 = base64_to_a32(ab_to_base64(derivedEncryptionKeyBytes));
                    var cipherObject = new sjcl.cipher.aes(derivedEncryptionKeyArray32);
                    var encryptedMasterKeyArray32 = encrypt_key(cipherObject, u_k);
                    var encryptedMasterKeyBase64 = a32_to_base64(encryptedMasterKeyArray32);

                    if (u_attr.k === encryptedMasterKeyBase64) {
                        return operation.reject(1);
                    }
                    else {
                        return operation.resolve();
                    }
                });
            return operation;
        }
        else {
            // registration v1
            var pw_aes = new sjcl.cipher.aes(prepare_key_pw(newPassword));
            var encryptedMasterKeyBase64 = a32_to_base64(encrypt_key(pw_aes, u_k));

            if (u_attr.k === encryptedMasterKeyBase64) {
                return operation.reject(1);
            }
            else {
                return operation.resolve();
            }
        }
    }
};