'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.Backend = exports.VERSION = undefined;

var _assign = require('babel-runtime/core-js/object/assign');

var _assign2 = _interopRequireDefault(_assign);

var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _createClass2 = require('babel-runtime/helpers/createClass');

var _createClass3 = _interopRequireDefault(_createClass2);

exports.upgradeDatabase = upgradeDatabase;

var _bluebird = require('bluebird');

var _bluebird2 = _interopRequireDefault(_bluebird);

var _utils = require('../../utils');

var _utils2 = _interopRequireDefault(_utils);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

var VERSION = exports.VERSION = 6;

/**
 * Implementation of a CryptoStore which is backed by an existing
 * IndexedDB connection. Generally you want IndexedDBCryptoStore
 * which connects to the database and defers to one of these.
 *
 * @implements {module:crypto/store/base~CryptoStore}
 */

var Backend = exports.Backend = function () {
    /**
     * @param {IDBDatabase} db
     */
    function Backend(db) {
        var _this = this;

        (0, _classCallCheck3.default)(this, Backend);

        this._db = db;

        // make sure we close the db on `onversionchange` - otherwise
        // attempts to delete the database will block (and subsequent
        // attempts to re-create it will also block).
        db.onversionchange = function (ev) {
            console.log('versionchange for indexeddb ' + _this._dbName + ': closing');
            db.close();
        };
    }

    /**
     * Look for an existing outgoing room key request, and if none is found,
     * add a new one
     *
     * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
     *
     * @returns {Promise} resolves to
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
     *    same instance as passed in, or the existing one.
     */


    (0, _createClass3.default)(Backend, [{
        key: 'getOrAddOutgoingRoomKeyRequest',
        value: function getOrAddOutgoingRoomKeyRequest(request) {
            var requestBody = request.requestBody;

            var deferred = _bluebird2.default.defer();
            var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
            txn.onerror = deferred.reject;

            // first see if we already have an entry for this request.
            this._getOutgoingRoomKeyRequest(txn, requestBody, function (existing) {
                if (existing) {
                    // this entry matches the request - return it.
                    console.log('already have key request outstanding for ' + (requestBody.room_id + ' / ' + requestBody.session_id + ': ') + 'not sending another');
                    deferred.resolve(existing);
                    return;
                }

                // we got to the end of the list without finding a match
                // - add the new request.
                console.log('enqueueing key request for ' + requestBody.room_id + ' / ' + requestBody.session_id);
                txn.oncomplete = function () {
                    deferred.resolve(request);
                };
                var store = txn.objectStore("outgoingRoomKeyRequests");
                store.add(request);
            });

            return deferred.promise;
        }

        /**
         * Look for an existing room key request
         *
         * @param {module:crypto~RoomKeyRequestBody} requestBody
         *    existing request to look for
         *
         * @return {Promise} resolves to the matching
         *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
         *    not found
         */

    }, {
        key: 'getOutgoingRoomKeyRequest',
        value: function getOutgoingRoomKeyRequest(requestBody) {
            var deferred = _bluebird2.default.defer();

            var txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
            txn.onerror = deferred.reject;

            this._getOutgoingRoomKeyRequest(txn, requestBody, function (existing) {
                deferred.resolve(existing);
            });
            return deferred.promise;
        }

        /**
         * look for an existing room key request in the db
         *
         * @private
         * @param {IDBTransaction} txn  database transaction
         * @param {module:crypto~RoomKeyRequestBody} requestBody
         *    existing request to look for
         * @param {Function} callback  function to call with the results of the
         *    search. Either passed a matching
         *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
         *    not found.
         */

    }, {
        key: '_getOutgoingRoomKeyRequest',
        value: function _getOutgoingRoomKeyRequest(txn, requestBody, callback) {
            var store = txn.objectStore("outgoingRoomKeyRequests");

            var idx = store.index("session");
            var cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]);

            cursorReq.onsuccess = function (ev) {
                var cursor = ev.target.result;
                if (!cursor) {
                    // no match found
                    callback(null);
                    return;
                }

                var existing = cursor.value;

                if (_utils2.default.deepCompare(existing.requestBody, requestBody)) {
                    // got a match
                    callback(existing);
                    return;
                }

                // look at the next entry in the index
                cursor.continue();
            };
        }

        /**
         * Look for room key requests by state
         *
         * @param {Array<Number>} wantedStates list of acceptable states
         *
         * @return {Promise} resolves to the a
         *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
         *    there are no pending requests in those states. If there are multiple
         *    requests in those states, an arbitrary one is chosen.
         */

    }, {
        key: 'getOutgoingRoomKeyRequestByState',
        value: function getOutgoingRoomKeyRequestByState(wantedStates) {
            if (wantedStates.length === 0) {
                return _bluebird2.default.resolve(null);
            }

            // this is a bit tortuous because we need to make sure we do the lookup
            // in a single transaction, to avoid having a race with the insertion
            // code.

            // index into the wantedStates array
            var stateIndex = 0;
            var result = void 0;

            function onsuccess(ev) {
                var cursor = ev.target.result;
                if (cursor) {
                    // got a match
                    result = cursor.value;
                    return;
                }

                // try the next state in the list
                stateIndex++;
                if (stateIndex >= wantedStates.length) {
                    // no matches
                    return;
                }

                var wantedState = wantedStates[stateIndex];
                var cursorReq = ev.target.source.openCursor(wantedState);
                cursorReq.onsuccess = onsuccess;
            }

            var txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
            var store = txn.objectStore("outgoingRoomKeyRequests");

            var wantedState = wantedStates[stateIndex];
            var cursorReq = store.index("state").openCursor(wantedState);
            cursorReq.onsuccess = onsuccess;

            return promiseifyTxn(txn).then(function () {
                return result;
            });
        }

        /**
         * Look for an existing room key request by id and state, and update it if
         * found
         *
         * @param {string} requestId      ID of request to update
         * @param {number} expectedState  state we expect to find the request in
         * @param {Object} updates        name/value map of updates to apply
         *
         * @returns {Promise} resolves to
         *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}
         *    updated request, or null if no matching row was found
         */

    }, {
        key: 'updateOutgoingRoomKeyRequest',
        value: function updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
            var result = null;

            function onsuccess(ev) {
                var cursor = ev.target.result;
                if (!cursor) {
                    return;
                }
                var data = cursor.value;
                if (data.state != expectedState) {
                    console.warn('Cannot update room key request from ' + expectedState + ' ' + ('as it was already updated to ' + data.state));
                    return;
                }
                (0, _assign2.default)(data, updates);
                cursor.update(data);
                result = data;
            }

            var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
            var cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
            cursorReq.onsuccess = onsuccess;
            return promiseifyTxn(txn).then(function () {
                return result;
            });
        }

        /**
         * Look for an existing room key request by id and state, and delete it if
         * found
         *
         * @param {string} requestId      ID of request to update
         * @param {number} expectedState  state we expect to find the request in
         *
         * @returns {Promise} resolves once the operation is completed
         */

    }, {
        key: 'deleteOutgoingRoomKeyRequest',
        value: function deleteOutgoingRoomKeyRequest(requestId, expectedState) {
            var txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
            var cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId);
            cursorReq.onsuccess = function (ev) {
                var cursor = ev.target.result;
                if (!cursor) {
                    return;
                }
                var data = cursor.value;
                if (data.state != expectedState) {
                    console.warn('Cannot delete room key request in state ' + data.state + ' ' + ('(expected ' + expectedState + ')'));
                    return;
                }
                cursor.delete();
            };
            return promiseifyTxn(txn);
        }

        // Olm Account

    }, {
        key: 'getAccount',
        value: function getAccount(txn, func) {
            var objectStore = txn.objectStore("account");
            var getReq = objectStore.get("-");
            getReq.onsuccess = function () {
                try {
                    func(getReq.result || null);
                } catch (e) {
                    abortWithException(txn, e);
                }
            };
        }
    }, {
        key: 'storeAccount',
        value: function storeAccount(txn, newData) {
            var objectStore = txn.objectStore("account");
            objectStore.put(newData, "-");
        }

        // Olm Sessions

    }, {
        key: 'countEndToEndSessions',
        value: function countEndToEndSessions(txn, func) {
            var objectStore = txn.objectStore("sessions");
            var countReq = objectStore.count();
            countReq.onsuccess = function () {
                func(countReq.result);
            };
        }
    }, {
        key: 'getEndToEndSessions',
        value: function getEndToEndSessions(deviceKey, txn, func) {
            var objectStore = txn.objectStore("sessions");
            var idx = objectStore.index("deviceKey");
            var getReq = idx.openCursor(deviceKey);
            var results = {};
            getReq.onsuccess = function () {
                var cursor = getReq.result;
                if (cursor) {
                    results[cursor.value.sessionId] = cursor.value.session;
                    cursor.continue();
                } else {
                    try {
                        func(results);
                    } catch (e) {
                        abortWithException(txn, e);
                    }
                }
            };
        }
    }, {
        key: 'getEndToEndSession',
        value: function getEndToEndSession(deviceKey, sessionId, txn, func) {
            var objectStore = txn.objectStore("sessions");
            var getReq = objectStore.get([deviceKey, sessionId]);
            getReq.onsuccess = function () {
                try {
                    if (getReq.result) {
                        func(getReq.result.session);
                    } else {
                        func(null);
                    }
                } catch (e) {
                    abortWithException(txn, e);
                }
            };
        }
    }, {
        key: 'storeEndToEndSession',
        value: function storeEndToEndSession(deviceKey, sessionId, session, txn) {
            var objectStore = txn.objectStore("sessions");
            objectStore.put({ deviceKey: deviceKey, sessionId: sessionId, session: session });
        }

        // Inbound group sessions

    }, {
        key: 'getEndToEndInboundGroupSession',
        value: function getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
            var objectStore = txn.objectStore("inbound_group_sessions");
            var getReq = objectStore.get([senderCurve25519Key, sessionId]);
            getReq.onsuccess = function () {
                try {
                    if (getReq.result) {
                        func(getReq.result.session);
                    } else {
                        func(null);
                    }
                } catch (e) {
                    abortWithException(txn, e);
                }
            };
        }
    }, {
        key: 'getAllEndToEndInboundGroupSessions',
        value: function getAllEndToEndInboundGroupSessions(txn, func) {
            var objectStore = txn.objectStore("inbound_group_sessions");
            var getReq = objectStore.openCursor();
            getReq.onsuccess = function () {
                var cursor = getReq.result;
                if (cursor) {
                    try {
                        func({
                            senderKey: cursor.value.senderCurve25519Key,
                            sessionId: cursor.value.sessionId,
                            sessionData: cursor.value.session
                        });
                    } catch (e) {
                        abortWithException(txn, e);
                    }
                    cursor.continue();
                } else {
                    try {
                        func(null);
                    } catch (e) {
                        abortWithException(txn, e);
                    }
                }
            };
        }
    }, {
        key: 'addEndToEndInboundGroupSession',
        value: function addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
            var objectStore = txn.objectStore("inbound_group_sessions");
            var addReq = objectStore.add({
                senderCurve25519Key: senderCurve25519Key, sessionId: sessionId, session: sessionData
            });
            addReq.onerror = function (ev) {
                if (addReq.error.name === 'ConstraintError') {
                    // This stops the error from triggering the txn's onerror
                    ev.stopPropagation();
                    // ...and this stops it from aborting the transaction
                    ev.preventDefault();
                    console.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId);
                } else {
                    abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error));
                }
            };
        }
    }, {
        key: 'storeEndToEndInboundGroupSession',
        value: function storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
            var objectStore = txn.objectStore("inbound_group_sessions");
            objectStore.put({
                senderCurve25519Key: senderCurve25519Key, sessionId: sessionId, session: sessionData
            });
        }
    }, {
        key: 'getEndToEndDeviceData',
        value: function getEndToEndDeviceData(txn, func) {
            var objectStore = txn.objectStore("device_data");
            var getReq = objectStore.get("-");
            getReq.onsuccess = function () {
                try {
                    func(getReq.result || null);
                } catch (e) {
                    abortWithException(txn, e);
                }
            };
        }
    }, {
        key: 'storeEndToEndDeviceData',
        value: function storeEndToEndDeviceData(deviceData, txn) {
            var objectStore = txn.objectStore("device_data");
            objectStore.put(deviceData, "-");
        }
    }, {
        key: 'storeEndToEndRoom',
        value: function storeEndToEndRoom(roomId, roomInfo, txn) {
            var objectStore = txn.objectStore("rooms");
            objectStore.put(roomInfo, roomId);
        }
    }, {
        key: 'getEndToEndRooms',
        value: function getEndToEndRooms(txn, func) {
            var rooms = {};
            var objectStore = txn.objectStore("rooms");
            var getReq = objectStore.openCursor();
            getReq.onsuccess = function () {
                var cursor = getReq.result;
                if (cursor) {
                    rooms[cursor.key] = cursor.value;
                    cursor.continue();
                } else {
                    try {
                        func(rooms);
                    } catch (e) {
                        abortWithException(txn, e);
                    }
                }
            };
        }
    }, {
        key: 'doTxn',
        value: function doTxn(mode, stores, func) {
            var txn = this._db.transaction(stores, mode);
            var promise = promiseifyTxn(txn);
            var result = func(txn);
            return promise.then(function () {
                return result;
            });
        }
    }]);
    return Backend;
}();

function upgradeDatabase(db, oldVersion) {
    console.log('Upgrading IndexedDBCryptoStore from version ' + oldVersion + (' to ' + VERSION));
    if (oldVersion < 1) {
        // The database did not previously exist.
        createDatabase(db);
    }
    if (oldVersion < 2) {
        db.createObjectStore("account");
    }
    if (oldVersion < 3) {
        var sessionsStore = db.createObjectStore("sessions", {
            keyPath: ["deviceKey", "sessionId"]
        });
        sessionsStore.createIndex("deviceKey", "deviceKey");
    }
    if (oldVersion < 4) {
        db.createObjectStore("inbound_group_sessions", {
            keyPath: ["senderCurve25519Key", "sessionId"]
        });
    }
    if (oldVersion < 5) {
        db.createObjectStore("device_data");
    }
    if (oldVersion < 6) {
        db.createObjectStore("rooms");
    }
    // Expand as needed.
}

function createDatabase(db) {
    var outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });

    // we assume that the RoomKeyRequestBody will have room_id and session_id
    // properties, to make the index efficient.
    outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]);

    outgoingRoomKeyRequestsStore.createIndex("state", "state");
}

/*
 * Aborts a transaction with a given exception
 * The transaction promise will be rejected with this exception.
 */
function abortWithException(txn, e) {
    // We cheekily stick our exception onto the transaction object here
    // We could alternatively make the thing we pass back to the app
    // an object containing the transaction and exception.
    txn._mx_abortexception = e;
    try {
        txn.abort();
    } catch (e) {
        // sometimes we won't be able to abort the transaction
        // (ie. if it's aborted or completed)
    }
}

function promiseifyTxn(txn) {
    return new _bluebird2.default(function (resolve, reject) {
        txn.oncomplete = function () {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            resolve();
        };
        txn.onerror = function () {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            reject();
        };
        txn.onabort = function () {
            return reject(txn._mx_abortexception);
        };
    });
}
//# sourceMappingURL=indexeddb-crypto-store-backend.js.map