<commit_before>/*
Copyright 2017 Vector Creations 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.
*/
"use strict";

import q from "q";
import {MatrixInMemoryStore} from "./memory";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
import utils from "../utils";

/**
 * This is an internal module. See {@link IndexedDBStore} for the public class.
 * @module store/indexeddb
 */

const VERSION = 1;

// If this value is too small we'll be writing very often which will cause
// noticable stop-the-world pauses. If this value is too big we'll be writing
// so infrequently that the /sync size gets bigger on reload. Writing more
// often does not affect the length of the pause since the entire /sync
// response is persisted each time.
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes

/**
 * Construct a new Indexed Database store backend. This requires a call to
 * connect() before this store can be used.
 * @constructor
 * @param {Object} indexedDBInterface The Indexed DB interface e.g
 * window.indexedDB
 * @param {string=} dbName Optional database name. The same name must be used
 * to open the same database.
 */
const IndexedDBStoreBackend = function IndexedDBStoreBackend(indexedDBInterface,
                                                             dbName) {
    this.indexedDB = indexedDBInterface;
    this._dbName = "matrix-js-sdk:" + (dbName || "default");
    this.db = null;
};

IndexedDBStoreBackend.prototype = {
    /**
     * Attempt to connect to the database. This can fail if the user does not
     * grant permission.
     * @return {Promise} Resolves if successfully connected.
     */
    connect: function() {
        if (this.db) {
            return q();
        }
        const req = this.indexedDB.open(this._dbName, VERSION);
        req.onupgradeneeded = (ev) => {
            const db = ev.target.result;
            const oldVersion = ev.oldVersion;
            if (oldVersion < 1) { // The database did not previously exist.
                createDatabase(db);
            }
            // Expand as needed.
        };

        return promiseifyRequest(req).then((ev) => {
            this.db = ev.target.result;

            // add a poorly-named listener for when deleteDatabase is called
            // so we can close our db connections.
            this.db.onversionchange = () => {
                this.db.close();
            };
        });
    },

    /**
     * Clear the entire database. This should be used when logging out of a client
     * to prevent mixing data between accounts.
     * @return {Promise} Resolved when the database is cleared.
     */
    clearDatabase: function() {
        console.log("Removing indexeddb instance: ", this._dbName);
        return promiseifyRequest(this.indexedDB.deleteDatabase(this._dbName));
    },

    /**
     * Persist rooms /sync data along with the next batch token.
     * @param {string} nextBatch The next_batch /sync value.
     * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
     * @return {Promise} Resolves if the data was persisted.
     */
    persistSyncData: function(nextBatch, roomsData) {
        console.log("Persisting sync data up to ", nextBatch);
        return q.try(() => {
            const txn = this.db.transaction(["sync"], "readwrite");
            const store = txn.objectStore("sync");
            store.put({
                clobber: "-", // constant key so will always clobber
                nextBatch: nextBatch,
                roomsData: roomsData,
            }); // put == UPSERT
            return promiseifyTxn(txn);
        });
    },

    /**
     * Persist a list of account data events. Events with the same 'type' will
     * be replaced.
     * @param {Object[]} accountData An array of raw user-scoped account data events
     * @return {Promise} Resolves if the events were persisted.
     */
    persistAccountData: function(accountData) {
        return q.try(() => {
            const txn = this.db.transaction(["accountData"], "readwrite");
            const store = txn.objectStore("accountData");
            for (let i = 0; i < accountData.length; i++) {
                store.put(accountData[i]); // put == UPSERT
            }
            return promiseifyTxn(txn);
        });
    },

    /**
     * Persist a list of User objects. Users with the same 'userId' will be
     * replaced.
     * @param {User[]} users An array of users
     * @return {Promise} Resolves if the users were persisted.
     */
    persistUsers: function(users) {
        return q.try(() => {
            const txn = this.db.transaction(["users"], "readwrite");
            const store = txn.objectStore("users");
            for (let i = 0; i < users.length; i++) {
                store.put({
                    userId: users[i].userId,
                    event: (users[i].events.presence ?
                                users[i].events.presence.event :
                                null),
                }); // put == UPSERT
            }
            return promiseifyTxn(txn);
        });
    },

    /**
     * Load all the users from the database. This is not cached.
     * @return {Promise<User[]>} A list of users.
     */
    loadUsers: function() {
        return q.try(() => {
            const txn = this.db.transaction(["users"], "readonly");
            const store = txn.objectStore("users");
            return selectQuery(store, undefined, (cursor) => {
                const user = new User(cursor.value.userId);
                if (cursor.value.event) {
                    user.setPresenceEvent(new MatrixEvent(cursor.value.event));
                }
                return user;
            });
        });
    },

    /**
     * Load all the account data events from the database. This is not cached.
     * @return {Promise<Object[]>} A list of raw global account events.
     */
    loadAccountData: function() {
        return q.try(() => {
            const txn = this.db.transaction(["accountData"], "readonly");
            const store = txn.objectStore("accountData");
            return selectQuery(store, undefined, (cursor) => {
                return cursor.value;
            });
        });
    },

    /**
     * Load the sync data from the database.
     * @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
     */
    loadSyncData: function() {
        return q.try(() => {
            const txn = this.db.transaction(["sync"], "readonly");
            const store = txn.objectStore("sync");
            return selectQuery(store, undefined, (cursor) => {
                return cursor.value;
            }).then((results) => {
                if (results.length > 1) {
                    console.warn("loadSyncData: More than 1 sync row found.");
                }
                return (results.length > 0 ? results[0] : {});
            });
        });
    },
};

/**
 * Construct a new Indexed Database store, which extends MatrixInMemoryStore.
 *
 * This store functions like a MatrixInMemoryStore except it periodically persists
 * the contents of the store to an IndexedDB backend.
 *
 * All data is still kept in-memory but can be loaded from disk by calling
 * startup(). This can make startup times quicker as a complete
 * sync from the server is not required. This does not reduce memory usage as all
 * the data is eagerly fetched when startup() is called.
 *

 * let syncAccumulator = new SyncAccumulator();
 * let opts = { localStorage: window.localStorage };
 * let store = new IndexedDBStore(
 *     new IndexedDBStoreBackend(window.indexedDB), syncAccumulator, opts
 * );
 * await store.startup(); // load from indexed db
 * let client = sdk.createClient({
 *     store: store,
 * });
 * client.startClient();
 * client.on("sync", function(state, prevState, data) {
 *     if (state === "PREPARED") {
 *         console.log("Started up, now with go faster stripes!");
 *     }
 * });
 *

 *
 * @constructor
 * @extends MatrixInMemoryStore
 * @param {IndexedDBStoreBackend} backend The indexed db backend instance.
 * @param {SyncAccumulator} syncAccumulator The sync accumulator which will be
 * loaded from IndexedDB and periodically saved to IndexedDB.
 * @param {Object=} opts Options for MatrixInMemoryStore.
 * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
 * this API if you need to perform specific indexeddb actions like deleting the
 * database.
 */
const IndexedDBStore = function IndexedDBStore(backend, syncAccumulator, opts) {
    MatrixInMemoryStore.call(this, opts);
    this.backend = backend;
    this.startedUp = false;
    this._syncTs = Date.now(); // updated when writes to the database are performed

    // internal structs to determine deltas for syncs to the database.
    this._userModifiedMap = {
        // user_id : timestamp
    };
    this._syncAccumulator = syncAccumulator;

    if (!this.backend || !this._syncAccumulator) {
        throw new Error("Missing backend or syncAccumulator");
    }
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);

/**
 * @return {Promise} Resolved when loaded from indexed db.
  */
IndexedDBStore.prototype.startup = function() {
    if (this.startedUp) {
        return q();
    }
    return this.backend.connect().then(() => {
        return q.all([
            this.backend.loadUsers(),
            this.backend.loadAccountData(),
            this.backend.loadSyncData(),
        ]);
    }).then((values) => {
        const [users, accountData, syncData] = values;
        console.log(
            "Loaded data from database: sync from ", syncData,
            " -- Reticulating splines...",
        );
        users.forEach((u) => {
            this._userModifiedMap[u.userId] = u.getLastModifiedTime();
            this.storeUser(u);
        });
        this._syncTs = Date.now(); // pretend we've written so we don't rewrite
        this.setSyncToken(syncData.nextBatch);
        this._setSyncData(syncData.nextBatch, syncData.roomsData, accountData);
    });
};

/**
 * Return the accumulator which will have the initial /sync data when startup()
 * is called.
 * @return {SyncAccumulator}
 */
IndexedDBStore.prototype.getSyncAccumulator = function() {
    return this._syncAccumulator;
};

/**
 * Delete all data from this store.
 * @return {Promise} Resolves if the data was deleted from the database.
 */
IndexedDBStore.prototype.deleteAllData = function() {
    MatrixInMemoryStore.prototype.deleteAllData.call(this);
    return this.backend.clearDatabase().then(() => {
        console.log("Deleted indexeddb data.");
    }, (err) => {
        console.error("Failed to delete indexeddb data: ", err);
        throw err;
    });
};

/**
 * Possibly write data to the database.
 * @return {Promise} Promise resolves after the write completes.
 */
IndexedDBStore.prototype.save = function() {
    const now = Date.now();
    if (now - this._syncTs > WRITE_DELAY_MS) {
        return this._syncToDatabase().catch((err) => {console.error("sync fail:", err);});
    }
    return q();
};

IndexedDBStore.prototype._setSyncData = function(nextBatch, roomsData, accountData) {
    this._syncAccumulator.accumulate({
        next_batch: nextBatch,
        rooms: roomsData,
        account_data: {
            events: accountData,
        },
    });
};

IndexedDBStore.prototype._syncToDatabase = function() {
    this._syncTs = Date.now(); // set now to guard against multi-writes

    // work out changed users (this doesn't handle deletions but you
    // can't 'delete' users as they are just presence events).
    const changedUsers = this.getUsers().filter((user) => {
        return this._userModifiedMap[user.userId] !== user.getLastModifiedTime();
    });
    changedUsers.forEach((u) => { // update times
        this._userModifiedMap[u.userId] = u.getLastModifiedTime();
    });

    const syncData = this._syncAccumulator.getJSON();

    return q.all([
        this.backend.persistUsers(changedUsers),
        this.backend.persistAccountData(syncData.accountData),
        this.backend.persistSyncData(syncData.nextBatch, syncData.roomsData),
    ]);
};

function createDatabase(db) {
    // Make user store, clobber based on user ID. (userId property of User objects)
    db.createObjectStore("users", { keyPath: ["userId"] });

    // Make account data store, clobber based on event type.
    // (event.type property of MatrixEvent objects)
    db.createObjectStore("accountData", { keyPath: ["type"] });

    // Make /sync store (sync tokens, room data, etc), always clobber (const key).
    db.createObjectStore("sync", { keyPath: ["clobber"] });
}

/**
 * Helper method to collect results from a Cursor and promiseify it.
 * @param {ObjectStore|Index} store The store to perform openCursor on.
 * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
 * @param {Function} resultMapper A function which is repeatedly called with a
 * Cursor.
 * Return the data you want to keep.
 * @return {Promise<T[]>} Resolves to an array of whatever you returned from
 * resultMapper.
 */
function selectQuery(store, keyRange, resultMapper) {
    const query = store.openCursor(keyRange);
    return q.Promise((resolve, reject) => { /*eslint new-cap: 0*/
        const results = [];
        query.onerror = (event) => {
            reject(new Error("Query failed: " + event.target.errorCode));
        };
        // collect results
        query.onsuccess = (event) => {
            const cursor = event.target.result;
            if (!cursor) {
                resolve(results);
                return; // end of results
            }
            results.push(resultMapper(cursor));
            cursor.continue();
        };
    });
}

function promiseifyTxn(txn) {
    return new q.Promise((resolve, reject) => {
        txn.oncomplete = function(event) {
            resolve(event);
        };
        txn.onerror = function(event) {
            reject(event);
        };
    });
}

function promiseifyRequest(req) {
    return new q.Promise((resolve, reject) => {
        req.onsuccess = function(event) {
            resolve(event);
        };
        req.onerror = function(event) {
            reject(event);
        };
    });
}

module.exports.IndexedDBStore = IndexedDBStore;
module.exports.IndexedDBStoreBackend = IndexedDBStoreBackend;
<commit_msg>Don't log the entire /sync response<commit_after>/*
Copyright 2017 Vector Creations 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.
*/
"use strict";

import q from "q";
import {MatrixInMemoryStore} from "./memory";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
import utils from "../utils";

/**
 * This is an internal module. See {@link IndexedDBStore} for the public class.
 * @module store/indexeddb
 */

const VERSION = 1;

// If this value is too small we'll be writing very often which will cause
// noticable stop-the-world pauses. If this value is too big we'll be writing
// so infrequently that the /sync size gets bigger on reload. Writing more
// often does not affect the length of the pause since the entire /sync
// response is persisted each time.
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes

/**
 * Construct a new Indexed Database store backend. This requires a call to
 * connect() before this store can be used.
 * @constructor
 * @param {Object} indexedDBInterface The Indexed DB interface e.g
 * window.indexedDB
 * @param {string=} dbName Optional database name. The same name must be used
 * to open the same database.
 */
const IndexedDBStoreBackend = function IndexedDBStoreBackend(indexedDBInterface,
                                                             dbName) {
    this.indexedDB = indexedDBInterface;
    this._dbName = "matrix-js-sdk:" + (dbName || "default");
    this.db = null;
};

IndexedDBStoreBackend.prototype = {
    /**
     * Attempt to connect to the database. This can fail if the user does not
     * grant permission.
     * @return {Promise} Resolves if successfully connected.
     */
    connect: function() {
        if (this.db) {
            return q();
        }
        const req = this.indexedDB.open(this._dbName, VERSION);
        req.onupgradeneeded = (ev) => {
            const db = ev.target.result;
            const oldVersion = ev.oldVersion;
            if (oldVersion < 1) { // The database did not previously exist.
                createDatabase(db);
            }
            // Expand as needed.
        };

        return promiseifyRequest(req).then((ev) => {
            this.db = ev.target.result;

            // add a poorly-named listener for when deleteDatabase is called
            // so we can close our db connections.
            this.db.onversionchange = () => {
                this.db.close();
            };
        });
    },

    /**
     * Clear the entire database. This should be used when logging out of a client
     * to prevent mixing data between accounts.
     * @return {Promise} Resolved when the database is cleared.
     */
    clearDatabase: function() {
        console.log("Removing indexeddb instance: ", this._dbName);
        return promiseifyRequest(this.indexedDB.deleteDatabase(this._dbName));
    },

    /**
     * Persist rooms /sync data along with the next batch token.
     * @param {string} nextBatch The next_batch /sync value.
     * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
     * @return {Promise} Resolves if the data was persisted.
     */
    persistSyncData: function(nextBatch, roomsData) {
        console.log("Persisting sync data up to ", nextBatch);
        return q.try(() => {
            const txn = this.db.transaction(["sync"], "readwrite");
            const store = txn.objectStore("sync");
            store.put({
                clobber: "-", // constant key so will always clobber
                nextBatch: nextBatch,
                roomsData: roomsData,
            }); // put == UPSERT
            return promiseifyTxn(txn);
        });
    },

    /**
     * Persist a list of account data events. Events with the same 'type' will
     * be replaced.
     * @param {Object[]} accountData An array of raw user-scoped account data events
     * @return {Promise} Resolves if the events were persisted.
     */
    persistAccountData: function(accountData) {
        return q.try(() => {
            const txn = this.db.transaction(["accountData"], "readwrite");
            const store = txn.objectStore("accountData");
            for (let i = 0; i < accountData.length; i++) {
                store.put(accountData[i]); // put == UPSERT
            }
            return promiseifyTxn(txn);
        });
    },

    /**
     * Persist a list of User objects. Users with the same 'userId' will be
     * replaced.
     * @param {User[]} users An array of users
     * @return {Promise} Resolves if the users were persisted.
     */
    persistUsers: function(users) {
        return q.try(() => {
            const txn = this.db.transaction(["users"], "readwrite");
            const store = txn.objectStore("users");
            for (let i = 0; i < users.length; i++) {
                store.put({
                    userId: users[i].userId,
                    event: (users[i].events.presence ?
                                users[i].events.presence.event :
                                null),
                }); // put == UPSERT
            }
            return promiseifyTxn(txn);
        });
    },

    /**
     * Load all the users from the database. This is not cached.
     * @return {Promise<User[]>} A list of users.
     */
    loadUsers: function() {
        return q.try(() => {
            const txn = this.db.transaction(["users"], "readonly");
            const store = txn.objectStore("users");
            return selectQuery(store, undefined, (cursor) => {
                const user = new User(cursor.value.userId);
                if (cursor.value.event) {
                    user.setPresenceEvent(new MatrixEvent(cursor.value.event));
                }
                return user;
            });
        });
    },

    /**
     * Load all the account data events from the database. This is not cached.
     * @return {Promise<Object[]>} A list of raw global account events.
     */
    loadAccountData: function() {
        return q.try(() => {
            const txn = this.db.transaction(["accountData"], "readonly");
            const store = txn.objectStore("accountData");
            return selectQuery(store, undefined, (cursor) => {
                return cursor.value;
            });
        });
    },

    /**
     * Load the sync data from the database.
     * @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
     */
    loadSyncData: function() {
        return q.try(() => {
            const txn = this.db.transaction(["sync"], "readonly");
            const store = txn.objectStore("sync");
            return selectQuery(store, undefined, (cursor) => {
                return cursor.value;
            }).then((results) => {
                if (results.length > 1) {
                    console.warn("loadSyncData: More than 1 sync row found.");
                }
                return (results.length > 0 ? results[0] : {});
            });
        });
    },
};

/**
 * Construct a new Indexed Database store, which extends MatrixInMemoryStore.
 *
 * This store functions like a MatrixInMemoryStore except it periodically persists
 * the contents of the store to an IndexedDB backend.
 *
 * All data is still kept in-memory but can be loaded from disk by calling
 * startup(). This can make startup times quicker as a complete
 * sync from the server is not required. This does not reduce memory usage as all
 * the data is eagerly fetched when startup() is called.
 *

 * let syncAccumulator = new SyncAccumulator();
 * let opts = { localStorage: window.localStorage };
 * let store = new IndexedDBStore(
 *     new IndexedDBStoreBackend(window.indexedDB), syncAccumulator, opts
 * );
 * await store.startup(); // load from indexed db
 * let client = sdk.createClient({
 *     store: store,
 * });
 * client.startClient();
 * client.on("sync", function(state, prevState, data) {
 *     if (state === "PREPARED") {
 *         console.log("Started up, now with go faster stripes!");
 *     }
 * });
 *

 *
 * @constructor
 * @extends MatrixInMemoryStore
 * @param {IndexedDBStoreBackend} backend The indexed db backend instance.
 * @param {SyncAccumulator} syncAccumulator The sync accumulator which will be
 * loaded from IndexedDB and periodically saved to IndexedDB.
 * @param {Object=} opts Options for MatrixInMemoryStore.
 * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
 * this API if you need to perform specific indexeddb actions like deleting the
 * database.
 */
const IndexedDBStore = function IndexedDBStore(backend, syncAccumulator, opts) {
    MatrixInMemoryStore.call(this, opts);
    this.backend = backend;
    this.startedUp = false;
    this._syncTs = Date.now(); // updated when writes to the database are performed

    // internal structs to determine deltas for syncs to the database.
    this._userModifiedMap = {
        // user_id : timestamp
    };
    this._syncAccumulator = syncAccumulator;

    if (!this.backend || !this._syncAccumulator) {
        throw new Error("Missing backend or syncAccumulator");
    }
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);

/**
 * @return {Promise} Resolved when loaded from indexed db.
  */
IndexedDBStore.prototype.startup = function() {
    if (this.startedUp) {
        return q();
    }
    return this.backend.connect().then(() => {
        return q.all([
            this.backend.loadUsers(),
            this.backend.loadAccountData(),
            this.backend.loadSyncData(),
        ]);
    }).then((values) => {
        const [users, accountData, syncData] = values;
        console.log(
            "Loaded data from database: sync from ", syncData.nextBatch,
            " -- Reticulating splines...",
        );
        users.forEach((u) => {
            this._userModifiedMap[u.userId] = u.getLastModifiedTime();
            this.storeUser(u);
        });
        this._syncTs = Date.now(); // pretend we've written so we don't rewrite
        this.setSyncToken(syncData.nextBatch);
        this._setSyncData(syncData.nextBatch, syncData.roomsData, accountData);
    });
};

/**
 * Return the accumulator which will have the initial /sync data when startup()
 * is called.
 * @return {SyncAccumulator}
 */
IndexedDBStore.prototype.getSyncAccumulator = function() {
    return this._syncAccumulator;
};

/**
 * Delete all data from this store.
 * @return {Promise} Resolves if the data was deleted from the database.
 */
IndexedDBStore.prototype.deleteAllData = function() {
    MatrixInMemoryStore.prototype.deleteAllData.call(this);
    return this.backend.clearDatabase().then(() => {
        console.log("Deleted indexeddb data.");
    }, (err) => {
        console.error("Failed to delete indexeddb data: ", err);
        throw err;
    });
};

/**
 * Possibly write data to the database.
 * @return {Promise} Promise resolves after the write completes.
 */
IndexedDBStore.prototype.save = function() {
    const now = Date.now();
    if (now - this._syncTs > WRITE_DELAY_MS) {
        return this._syncToDatabase().catch((err) => {console.error("sync fail:", err);});
    }
    return q();
};

IndexedDBStore.prototype._setSyncData = function(nextBatch, roomsData, accountData) {
    this._syncAccumulator.accumulate({
        next_batch: nextBatch,
        rooms: roomsData,
        account_data: {
            events: accountData,
        },
    });
};

IndexedDBStore.prototype._syncToDatabase = function() {
    this._syncTs = Date.now(); // set now to guard against multi-writes

    // work out changed users (this doesn't handle deletions but you
    // can't 'delete' users as they are just presence events).
    const changedUsers = this.getUsers().filter((user) => {
        return this._userModifiedMap[user.userId] !== user.getLastModifiedTime();
    });
    changedUsers.forEach((u) => { // update times
        this._userModifiedMap[u.userId] = u.getLastModifiedTime();
    });

    const syncData = this._syncAccumulator.getJSON();

    return q.all([
        this.backend.persistUsers(changedUsers),
        this.backend.persistAccountData(syncData.accountData),
        this.backend.persistSyncData(syncData.nextBatch, syncData.roomsData),
    ]);
};

function createDatabase(db) {
    // Make user store, clobber based on user ID. (userId property of User objects)
    db.createObjectStore("users", { keyPath: ["userId"] });

    // Make account data store, clobber based on event type.
    // (event.type property of MatrixEvent objects)
    db.createObjectStore("accountData", { keyPath: ["type"] });

    // Make /sync store (sync tokens, room data, etc), always clobber (const key).
    db.createObjectStore("sync", { keyPath: ["clobber"] });
}

/**
 * Helper method to collect results from a Cursor and promiseify it.
 * @param {ObjectStore|Index} store The store to perform openCursor on.
 * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
 * @param {Function} resultMapper A function which is repeatedly called with a
 * Cursor.
 * Return the data you want to keep.
 * @return {Promise<T[]>} Resolves to an array of whatever you returned from
 * resultMapper.
 */
function selectQuery(store, keyRange, resultMapper) {
    const query = store.openCursor(keyRange);
    return q.Promise((resolve, reject) => { /*eslint new-cap: 0*/
        const results = [];
        query.onerror = (event) => {
            reject(new Error("Query failed: " + event.target.errorCode));
        };
        // collect results
        query.onsuccess = (event) => {
            const cursor = event.target.result;
            if (!cursor) {
                resolve(results);
                return; // end of results
            }
            results.push(resultMapper(cursor));
            cursor.continue();
        };
    });
}

function promiseifyTxn(txn) {
    return new q.Promise((resolve, reject) => {
        txn.oncomplete = function(event) {
            resolve(event);
        };
        txn.onerror = function(event) {
            reject(event);
        };
    });
}

function promiseifyRequest(req) {
    return new q.Promise((resolve, reject) => {
        req.onsuccess = function(event) {
            resolve(event);
        };
        req.onerror = function(event) {
            reject(event);
        };
    });
}

module.exports.IndexedDBStore = IndexedDBStore;
module.exports.IndexedDBStoreBackend = IndexedDBStoreBackend;
<|endoftext|>


<commit_before>import { observable, computed, action, reaction } from 'mobx';

import esES from '../language/i18n/es';
import enUS from '../language/i18n/en';

export default class LanguageStore {
  @observable language;
  @observable resource;

  constructor(language) {
    // Set the Initial State of Language Observable
    this.language = language;
    // Set the Initial State of Resource Observable
    switch (this.language) {
      case 'en':
        this.resource = enUS;
        break;
      case 'es':
        this.resource = esES;
        break;
      default:
        this.resource = enUS;
        break;
    }
    // Stablish a Reaction for changing Resources on the fly when Language Observer Changes
    reaction(
      () => this.language,
      () => {
        switch (this.language) {
          case 'en':
            this.resource = enUS;
            break;
          case 'es':
            this.resource = esES;
            break;
          default:
            this.resource = enUS;
            break;
        }
      }
    );
  }

  @action("Change language to") changeLanguageTo(language) {
    this.language = language;
  }

  @computed get currentLanguage() {
    return this.language;
  }
}
<commit_msg>Updated LanguageStore to achieve a better performance with @computed instead of @observable for resource object<commit_after>import { observable, computed, action, reaction } from 'mobx';

import esES from '../language/i18n/es';
import enUS from '../language/i18n/en';

export default class LanguageStore {
  @observable language;

  @computed get resource() {
    switch (this.language) {
      case 'en':
        return enUS;
        break;
      case 'es':
        return esES;
        break;
      default:
        return enUS;
        break;
    }
  }

  constructor(language) {
    // Set the Initial State of Language Observable
    this.language = language;
  }

  @action('Change language to')
  changeLanguageTo(language) {
    this.language = language;
  }

  @computed get currentLanguage() {
    return this.language;
  }
}
<|endoftext|>


<commit_before>const mongoose = require('mongoose')
const config = require('../../config.js')
const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const log = require('../../util/logger.js')

/**
 * @typedef {import('mongoose').Model<import('mongoose').Document, {}>} MongooseModel
 */

class Base {
  /**
   * Object data
   * @param {MongooseModel|Object<string, any>} data
   */
  constructor (data = {}, _saved = false) {
    /**
     * this.data must be serialized/deserialized to maintain
     * equal function between database and databaseless
     * @type {Object<string, any>}
     */
    this.data = data instanceof mongoose.Model ? JSON.parse(JSON.stringify(data.toJSON())) : data

    /**
     * Internal ID, usually MongoDB's ObjectId purely used
     * to distinguish between documents and to grab from
     * database during testing.
     * @type {string}
     */
    this._id = this.data._id

    /**
     * Whether this has been saved to the database already
     * @type {boolean}
     */
    this._saved = _saved

    /**
     * Only used for database methods
     * @type {MongooseModel}
     */
    this.document = data instanceof mongoose.Model ? data : null

    /**
     * The bot version this data model was created on
     * @type {string}
     */
    this.version = this.getField('version')

    // Run the get method and throw its associated error if unimplemented
    void this.constructor.Model
  }

  /**
   * Remove the internal version key when finding from database
   */
  static FIND_PROJECTION () {
    return { __v: 0 }
  }

  /**
   * Returns the Mongoose model specific to the class
   * @returns {MongooseModel}
   */
  static get Model () {
    throw new Error('Model static get method must be implemented by subclasses')
  }

  /**
   * Checks whether the app is databaseless
   * @returns {boolean}
   */
  static get isMongoDatabase () {
    return config.database.uri.startsWith('mongo')
  }

  /**
   * Get the folder paths of the intended fs location the data will
   * be written to.
   * @returns {string[]}
   */
  static getFolderPaths () {
    const folderPath = config.database.uri
    const subfolderPath = path.join(folderPath, this.Model.collection.collectionName)
    return [ folderPath, subfolderPath ]
  }

  /**
   * Helper function to return undefined for empty objects
   * @param {any} field - The field name
   * @private
   */
  static resolveObject (value) {
    if (!value || Object.keys(value).length === 0) {
      return undefined
    } else {
      return value
    }
  }

  /**
   * A function that validates data before saving it.
   * Used by extended classes.
   */
  async validate () {}

  /**
   * Resolves data acquisition based on whether the app is databaseless
   * @param {string} field - The field to get
   * @param {*} def - The default value if there is no value
   * @returns {*} - The field value
   */
  getField (field, def) {
    const value = this.data[field]
    return value === undefined || value === null ? def : value
  }

  /**
   * Convert data into a plain object
   * @returns {Object<string, any>} - The plain object
   */
  toObject () {
    throw new Error('Method must be implemented by subclasses')
  }

  /**
   * Get a document
   * @param {string} id - Guild ID
   * @returns {Base|null}
   */
  static async get (id) {
    if (!id) {
      throw new Error('Undefined id')
    }
    if (typeof id !== 'string') {
      throw new Error('id must be a string')
    }

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const doc = await DatabaseModel.findById(id).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const filePath = path.join(config.database.uri, `${id}.json`)
    if (!fs.existsSync(filePath)) {
      return null
    }

    try {
      const readContent = fs.readFileSync(filePath)
      return new this(JSON.parse(readContent), true)
    } catch (err) {
      log.general.warning(`Could not parse ${DatabaseModel.collection.collectionName} JSON from file ${id}`, err)
      return null
    }
  }

  /**
   * Get by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base|null}
   */
  static async getBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const doc = await DatabaseModel.findOne(query, this.FIND_PROJECTION).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const many = await this.getManyBy(field, value)
    return many[0] ? new this(many[0], true) : null
  }

  /**
   * Get many by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base[]}
   */
  static async getManyBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const docs = await DatabaseModel.find(query, this.FIND_PROJECTION).exec()
      return docs.map(doc => new this(doc, true))
    }

    // Databaseless - very slow
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(folderPath)) {
      return []
    }

    const fileNames = await fsPromises.readdir(folderPath)
    const promises = fileNames.map(name => fsPromises.readFile(path.join(folderPath, name)))
    const resolved = await Promise.all(promises)
    const jsons = resolved.map((contents, index) => {
      try {
        return JSON.parse(contents)
      } catch (err) {
        log.general.error(`Failed to parse json at ${folderPath} ${fileNames[index]}`, err)
      }
    })
    return jsons
      .filter(item => item && item[field] === value)
      .map(item => new this(item, true))
  }

  /**
   * Get multiple documents
   * @param {string[]} ids - Array of guild IDs
   * @returns {Base[]}
   */
  static async getMany (ids) {
    return Promise.all(ids.map(id => this.get(id)))
  }

  /**
   * Get all documents
   * @returns {Base[]}
   */
  static async getAll () {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const documents = await DatabaseModel.find({}, this.FIND_PROJECTION).exec()
      return documents.map(doc => new this(doc))
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const fullPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(fullPath)) {
      return []
    }
    const fileReads = fs.readdirSync(fullPath)
      .filter(id => /\.json$/.test(id))
      .map(fileName => this.get(fileName.replace(/\.json/i, '')))

    return Promise.all(fileReads)
  }

  /**
   * Deletes this from either the database from the file system
   * depending on whether the app is databaseless.
   */
  async delete () {
    if (!this._saved) {
      throw new Error('Data has not been saved')
    }

    // Mongo
    if (this.constructor.isMongoDatabase) {
      await this.document.remove()
      return
    }

    const Model = this.constructor.Model

    // Databaseless
    const paths = this.constructor.getFolderPaths()
    const folderPath = paths[paths.length - 1]
    const filePath = path.join(folderPath, `${this._id}.json`)
    if (!fs.existsSync(filePath)) {
      log.general.warning(`Unable to delete ${Model.collection.collectionName} ${this._id} at ${filePath} since its nonexistent`)
    } else {
      fs.unlinkSync(filePath)
    }
  }

  /**
   * Save the data to either the database or a file depending on whether the
   * app is databaseless.
   * @returns {Base} - This instance
   */
  async save () {
    await this.validate()
    if (this.constructor.isMongoDatabase) {
      return this.saveToDatabase()
    } else {
      return this.saveToFile()
    }
  }

  /**
   * Save the data to the database
   * @returns {Base}
   */
  async saveToDatabase () {
    const toSave = this.toObject()

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.constructor.Model

    if (!this._saved) {
      // Delete all undefined keys
      for (const key in toSave) {
        if (toSave[key] === undefined) {
          delete toSave[key]
        }
      }
      const model = new DatabaseModel(toSave)
      const document = await model.save()

      this._saved = true
      this._id = document.id
      this.document = document
      this.data = JSON.parse(JSON.stringify(document.toJSON()))
    } else {
      for (const key in toSave) {
        const value = toSave[key]
        // Map values must be individually set and deleted
        if (value instanceof Map) {
          const docMap = this.document[key]
          // First remove all unknown keys
          docMap.forEach((v, key) => {
            if (!value.has(key)) {
              docMap.delete(key)
            }
          })
          // Then set the new values
          value.forEach((value, mapKey) => docMap.set(mapKey, value))
        } else {
          this.document.set(key, toSave[key])
        }
      }
      const saved = await this.document.save()
      this.data = JSON.parse(JSON.stringify(saved.toJSON()))

      // Update class data
      for (const key in toSave) {
        this[key] = this.data[key]
      }
    }

    return this
  }

  /**
   * Saves the data to a file
   * @returns {Base}
   */
  async saveToFile () {
    const toSave = this.toObject()

    for (const key in toSave) {
      if (toSave[key] === undefined) {
        delete toSave[key]
      }
    }
    const serialized = JSON.stringify(toSave, null, 2)

    const folderPaths = this.constructor.getFolderPaths()
    for (const p of folderPaths) {
      if (!fs.existsSync(p)) {
        fs.mkdirSync(p)
      }
    }
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!this._saved) {
      const newId = new mongoose.Types.ObjectId().toHexString()
      await fs.writeFileSync(path.join(folderPath, `${newId}.json`), serialized)
      this._id = newId
      this._saved = true
    } else {
      await fs.writeFileSync(path.join(folderPath, `${this._id}.json`), serialized)
    }
    return this
  }
}

module.exports = Base
<commit_msg>Add failsafe if map is missing<commit_after>const mongoose = require('mongoose')
const config = require('../../config.js')
const fs = require('fs')
const fsPromises = fs.promises
const path = require('path')
const log = require('../../util/logger.js')

/**
 * @typedef {import('mongoose').Model<import('mongoose').Document, {}>} MongooseModel
 */

class Base {
  /**
   * Object data
   * @param {MongooseModel|Object<string, any>} data
   */
  constructor (data = {}, _saved = false) {
    /**
     * this.data must be serialized/deserialized to maintain
     * equal function between database and databaseless
     * @type {Object<string, any>}
     */
    this.data = data instanceof mongoose.Model ? JSON.parse(JSON.stringify(data.toJSON())) : data

    /**
     * Internal ID, usually MongoDB's ObjectId purely used
     * to distinguish between documents and to grab from
     * database during testing.
     * @type {string}
     */
    this._id = this.data._id

    /**
     * Whether this has been saved to the database already
     * @type {boolean}
     */
    this._saved = _saved

    /**
     * Only used for database methods
     * @type {MongooseModel}
     */
    this.document = data instanceof mongoose.Model ? data : null

    /**
     * The bot version this data model was created on
     * @type {string}
     */
    this.version = this.getField('version')

    // Run the get method and throw its associated error if unimplemented
    void this.constructor.Model
  }

  /**
   * Remove the internal version key when finding from database
   */
  static FIND_PROJECTION () {
    return { __v: 0 }
  }

  /**
   * Returns the Mongoose model specific to the class
   * @returns {MongooseModel}
   */
  static get Model () {
    throw new Error('Model static get method must be implemented by subclasses')
  }

  /**
   * Checks whether the app is databaseless
   * @returns {boolean}
   */
  static get isMongoDatabase () {
    return config.database.uri.startsWith('mongo')
  }

  /**
   * Get the folder paths of the intended fs location the data will
   * be written to.
   * @returns {string[]}
   */
  static getFolderPaths () {
    const folderPath = config.database.uri
    const subfolderPath = path.join(folderPath, this.Model.collection.collectionName)
    return [ folderPath, subfolderPath ]
  }

  /**
   * Helper function to return undefined for empty objects
   * @param {any} field - The field name
   * @private
   */
  static resolveObject (value) {
    if (!value || Object.keys(value).length === 0) {
      return undefined
    } else {
      return value
    }
  }

  /**
   * A function that validates data before saving it.
   * Used by extended classes.
   */
  async validate () {}

  /**
   * Resolves data acquisition based on whether the app is databaseless
   * @param {string} field - The field to get
   * @param {*} def - The default value if there is no value
   * @returns {*} - The field value
   */
  getField (field, def) {
    const value = this.data[field]
    return value === undefined || value === null ? def : value
  }

  /**
   * Convert data into a plain object
   * @returns {Object<string, any>} - The plain object
   */
  toObject () {
    throw new Error('Method must be implemented by subclasses')
  }

  /**
   * Get a document
   * @param {string} id - Guild ID
   * @returns {Base|null}
   */
  static async get (id) {
    if (!id) {
      throw new Error('Undefined id')
    }
    if (typeof id !== 'string') {
      throw new Error('id must be a string')
    }

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const doc = await DatabaseModel.findById(id).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const filePath = path.join(config.database.uri, `${id}.json`)
    if (!fs.existsSync(filePath)) {
      return null
    }

    try {
      const readContent = fs.readFileSync(filePath)
      return new this(JSON.parse(readContent), true)
    } catch (err) {
      log.general.warning(`Could not parse ${DatabaseModel.collection.collectionName} JSON from file ${id}`, err)
      return null
    }
  }

  /**
   * Get by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base|null}
   */
  static async getBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const doc = await DatabaseModel.findOne(query, this.FIND_PROJECTION).exec()
      return doc ? new this(doc, true) : null
    }

    // Databaseless
    const many = await this.getManyBy(field, value)
    return many[0] ? new this(many[0], true) : null
  }

  /**
   * Get many by a field's value
   * @param {string} field - Field name
   * @param {any} value - Field value
   * @returns {Base[]}
   */
  static async getManyBy (field, value) {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Database
    if (this.isMongoDatabase) {
      const query = {
        [field]: value
      }
      const docs = await DatabaseModel.find(query, this.FIND_PROJECTION).exec()
      return docs.map(doc => new this(doc, true))
    }

    // Databaseless - very slow
    const folderPaths = this.getFolderPaths()
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(folderPath)) {
      return []
    }

    const fileNames = await fsPromises.readdir(folderPath)
    const promises = fileNames.map(name => fsPromises.readFile(path.join(folderPath, name)))
    const resolved = await Promise.all(promises)
    const jsons = resolved.map((contents, index) => {
      try {
        return JSON.parse(contents)
      } catch (err) {
        log.general.error(`Failed to parse json at ${folderPath} ${fileNames[index]}`, err)
      }
    })
    return jsons
      .filter(item => item && item[field] === value)
      .map(item => new this(item, true))
  }

  /**
   * Get multiple documents
   * @param {string[]} ids - Array of guild IDs
   * @returns {Base[]}
   */
  static async getMany (ids) {
    return Promise.all(ids.map(id => this.get(id)))
  }

  /**
   * Get all documents
   * @returns {Base[]}
   */
  static async getAll () {
    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.Model

    // Mongo
    if (this.isMongoDatabase) {
      const documents = await DatabaseModel.find({}, this.FIND_PROJECTION).exec()
      return documents.map(doc => new this(doc))
    }

    // Databaseless
    const folderPaths = this.getFolderPaths()
    const fullPath = folderPaths[folderPaths.length - 1]
    if (!fs.existsSync(fullPath)) {
      return []
    }
    const fileReads = fs.readdirSync(fullPath)
      .filter(id => /\.json$/.test(id))
      .map(fileName => this.get(fileName.replace(/\.json/i, '')))

    return Promise.all(fileReads)
  }

  /**
   * Deletes this from either the database from the file system
   * depending on whether the app is databaseless.
   */
  async delete () {
    if (!this._saved) {
      throw new Error('Data has not been saved')
    }

    // Mongo
    if (this.constructor.isMongoDatabase) {
      await this.document.remove()
      return
    }

    const Model = this.constructor.Model

    // Databaseless
    const paths = this.constructor.getFolderPaths()
    const folderPath = paths[paths.length - 1]
    const filePath = path.join(folderPath, `${this._id}.json`)
    if (!fs.existsSync(filePath)) {
      log.general.warning(`Unable to delete ${Model.collection.collectionName} ${this._id} at ${filePath} since its nonexistent`)
    } else {
      fs.unlinkSync(filePath)
    }
  }

  /**
   * Save the data to either the database or a file depending on whether the
   * app is databaseless.
   * @returns {Base} - This instance
   */
  async save () {
    await this.validate()
    if (this.constructor.isMongoDatabase) {
      return this.saveToDatabase()
    } else {
      return this.saveToFile()
    }
  }

  /**
   * Save the data to the database
   * @returns {Base}
   */
  async saveToDatabase () {
    const toSave = this.toObject()

    /**
     * @type {MongooseModel}
     */
    const DatabaseModel = this.constructor.Model

    if (!this._saved) {
      // Delete all undefined keys
      for (const key in toSave) {
        if (toSave[key] === undefined) {
          delete toSave[key]
        }
      }
      const model = new DatabaseModel(toSave)
      const document = await model.save()

      this._saved = true
      this._id = document.id
      this.document = document
      this.data = JSON.parse(JSON.stringify(document.toJSON()))
    } else {
      for (const key in toSave) {
        const value = toSave[key]
        // Map values must be individually set and deleted
        if (value instanceof Map) {
          if (!this.document[key]) {
            this.document.set(key, new Map())
          }
          const docMap = this.document[key]
          // First remove all unknown keys
          docMap.forEach((v, key) => {
            if (!value.has(key)) {
              docMap.delete(key)
            }
          })
          // Then set the new values
          value.forEach((value, mapKey) => docMap.set(mapKey, value))
        } else {
          this.document.set(key, toSave[key])
        }
      }
      const saved = await this.document.save()
      this.data = JSON.parse(JSON.stringify(saved.toJSON()))

      // Update class data
      for (const key in toSave) {
        this[key] = this.data[key]
      }
    }

    return this
  }

  /**
   * Saves the data to a file
   * @returns {Base}
   */
  async saveToFile () {
    const toSave = this.toObject()

    for (const key in toSave) {
      if (toSave[key] === undefined) {
        delete toSave[key]
      }
    }
    const serialized = JSON.stringify(toSave, null, 2)

    const folderPaths = this.constructor.getFolderPaths()
    for (const p of folderPaths) {
      if (!fs.existsSync(p)) {
        fs.mkdirSync(p)
      }
    }
    const folderPath = folderPaths[folderPaths.length - 1]
    if (!this._saved) {
      const newId = new mongoose.Types.ObjectId().toHexString()
      await fs.writeFileSync(path.join(folderPath, `${newId}.json`), serialized)
      this._id = newId
      this._saved = true
    } else {
      await fs.writeFileSync(path.join(folderPath, `${this._id}.json`), serialized)
    }
    return this
  }
}

module.exports = Base
<|endoftext|>


<commit_before>let Promise
try {
  Promise = require('bluebird')
} catch (err) {
  Promise = global.Promise
}

const { delay: promDelay } = require('../util')

/**
 * Built-in base helper class
 * @abstract
 * @prop {?Logger} logger Logger instance
 * @prop {?Interpreter} i18n Interpreter instance
 */
class Base {
  /**
   * Creates a new Base instance
   * @arg {Client} client Client instance
   */
  constructor (client) {
    this._client = client
    this.logger = client.logger
    this.i18n = client.plugins.get('i18n')
  }

  /**
   * Checks if a user has certain permissions in a channel
   * @arg {external:"Eris.GuildChannel"} channel The channel to check in
   * @arg {external:"Eris.User"} user The user to check
   * @arg {...String} perms The permission(s) to check
   * @returns {Boolean}
   */
  hasPermissions (channel, user, ...perms) {
    const member = channel.guild.members.get(user.id);
    for (const perm of perms) {
      if (!member.permission.has(perm)) return false;
    }
    return true;
  }

  /**
   * Parses a string for localisation
   * @arg {String} content The string to parse
   * @arg {String} [lang='en'] The locale name
   * @arg {Object} [tags] Additional interpolation tags
   */
  t (content = '', lang = 'en', tags = {}) {
    if (!this.i18n) return content
    const file = this.name ? this.name.split(':')[0] : (this.labels ? this.labels[0] : 'common')
    return this.i18n.parse(content, this.localeKey || file || null, lang, tags)
  }

  /**
   * Sends a message to a channel
   * @arg {external:"Eris.GuildChannel"} channel The channel to send the message in
   * @arg {String} content The string to send
   * @arg {Object} [options] Message options
   * @arg {String} [options.lang] Message locale
   * @arg {Number} [options.delay=0] Delay to send the message, set to 0 for no delay
   * @arg {Number} [options.deleteDelay=0] Delay to delete the message after it's sent, set to 0 for no deletion
   * @arg {Object} [options.file] A file object
   * @arg {String} [options.file.name] Name of file
   * @arg {Buffer} [options.file.file] Buffer containing file data
   * @arg {Object} [options.embed] Embed object, see {@link https://discordapp.com/developers/docs/resources/channel#embed-object|official Discord API documentation}
   * @returns {Promise.<external:"Eris.Message"|null>}
   */
  async send (chan, content, options = {}) {
    const channel = typeof chan === 'string' ? this._client.getChannel(chan) : chan
    if (!channel) {
      const err = new Error(`Could not send message: Invalid channel - ${chan}`)
      if (this.logger) {
        this.logger.error(err)
        return
      } else {
        throw err
      }
    }

    let { file = null, lang, delay = 0, deleteDelay = 0, embed } = options
    if (channel.guild && !this.hasPermissions(channel, this._client.user, 'sendMessages')) {
      const err = new Error('Could not send message: Insufficient permissions')
      if (this.logger) {
        this.logger.error(err)
        return
      } else {
        throw err
      }
    }

    if (delay) await promDelay(delay)

    if (Array.isArray(content)) content = content.join('\n')
    if (this.i18n) content = this.t(content, lang, options)
    content = content.match(/(.|[\r\n]){1,2000}/g)

    try {
      if (!content || !content.length) {
        const msg = await channel.createMessage({ embed, content: '' }, file)
        return deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg
      }

      let msg
      for (const c of content) {
        const firstMsg = await channel.createMessage(!msg ? { embed, content: c } : c, !msg ? file : null)
        .then(msg => deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg)
        msg = firstMsg
      }

      return msg
    } catch (err) {
      if (this.logger) {
        this.logger.error('Could not send message -', err)
      } else {
        throw err
      }
    }
  }

  /**
   * Edits a message with updated content
   * @arg {external:"Eris.Message"} message The message to edit
   * @arg {String} content The content to edit the message with
   * @arg {Object} [options] Options object
   * @arg {String} [options.lang] Locale of content
   * @arg {Number} [options.delay] Delay to edit the message
   * @returns {Promise}
   */
  edit (msg, content, options) {
    const { lang, delay = 0 } = options

    if (Array.isArray(content)) content = content.join('\n')
    if (this.i18n) content = this.t(content, lang, options)

    return (delay ? promDelay(delay) : Promise.resolve()).then(() => msg.edit(content))
  }

  /**
   * Deletes multiple messages if there are sufficient permissions
   * @arg {...external:"Eris.Message"} messages The messages to delete
   * @returns {Promise}
   */
  deleteMessages (...msgs) {
    const id = this._client.user.id
    return Promise.all(msgs.reduce((arr, msg) => {
      if (!msg || !msg.channel.guild) return arr
      if (msg.author.id === id || this.hasPermissions(msg.channel, msg.member, 'manageMessages')) {
        arr.push(msg.delete())
      }
      return arr
    }, []))
  }
}

module.exports = Base
<commit_msg>Fix hasPermissions check in PMs<commit_after>let Promise
try {
  Promise = require('bluebird')
} catch (err) {
  Promise = global.Promise
}

const { delay: promDelay } = require('../util')

/**
 * Built-in base helper class
 * @abstract
 * @prop {?Logger} logger Logger instance
 * @prop {?Interpreter} i18n Interpreter instance
 */
class Base {
  /**
   * Creates a new Base instance
   * @arg {Client} client Client instance
   */
  constructor (client) {
    this._client = client
    this.logger = client.logger
    this.i18n = client.plugins.get('i18n')
  }

  /**
   * Checks if a user has certain permissions in a channel
   * @arg {external:"Eris.GuildChannel"} channel The channel to check in
   * @arg {external:"Eris.User"} user The user to check
   * @arg {...String} perms The permission(s) to check
   * @returns {Boolean}
   */
  hasPermissions (channel, user, ...perms) {
    if (!channel.guild) return true;
    const member = channel.guild.members.get(user.id);
    for (const perm of perms) {
      if (!member.permission.has(perm)) return false;
    }
    return true;
  }

  /**
   * Parses a string for localisation
   * @arg {String} content The string to parse
   * @arg {String} [lang='en'] The locale name
   * @arg {Object} [tags] Additional interpolation tags
   */
  t (content = '', lang = 'en', tags = {}) {
    if (!this.i18n) return content
    const file = this.name ? this.name.split(':')[0] : (this.labels ? this.labels[0] : 'common')
    return this.i18n.parse(content, this.localeKey || file || null, lang, tags)
  }

  /**
   * Sends a message to a channel
   * @arg {external:"Eris.GuildChannel"} channel The channel to send the message in
   * @arg {String} content The string to send
   * @arg {Object} [options] Message options
   * @arg {String} [options.lang] Message locale
   * @arg {Number} [options.delay=0] Delay to send the message, set to 0 for no delay
   * @arg {Number} [options.deleteDelay=0] Delay to delete the message after it's sent, set to 0 for no deletion
   * @arg {Object} [options.file] A file object
   * @arg {String} [options.file.name] Name of file
   * @arg {Buffer} [options.file.file] Buffer containing file data
   * @arg {Object} [options.embed] Embed object, see {@link https://discordapp.com/developers/docs/resources/channel#embed-object|official Discord API documentation}
   * @returns {Promise.<external:"Eris.Message"|null>}
   */
  async send (chan, content, options = {}) {
    const channel = typeof chan === 'string' ? this._client.getChannel(chan) : chan
    if (!channel) {
      const err = new Error(`Could not send message: Invalid channel - ${chan}`)
      if (this.logger) {
        this.logger.error(err)
        return
      } else {
        throw err
      }
    }

    let { file = null, lang, delay = 0, deleteDelay = 0, embed } = options
    if (channel.guild && !this.hasPermissions(channel, this._client.user, 'sendMessages')) {
      const err = new Error('Could not send message: Insufficient permissions')
      if (this.logger) {
        this.logger.error(err)
        return
      } else {
        throw err
      }
    }

    if (delay) await promDelay(delay)

    if (Array.isArray(content)) content = content.join('\n')
    if (this.i18n) content = this.t(content, lang, options)
    content = content.match(/(.|[\r\n]){1,2000}/g)

    try {
      if (!content || !content.length) {
        const msg = await channel.createMessage({ embed, content: '' }, file)
        return deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg
      }

      let msg
      for (const c of content) {
        const firstMsg = await channel.createMessage(!msg ? { embed, content: c } : c, !msg ? file : null)
        .then(msg => deleteDelay ? promDelay(deleteDelay).then(() => msg.delete()) : msg)
        msg = firstMsg
      }

      return msg
    } catch (err) {
      if (this.logger) {
        this.logger.error('Could not send message -', err)
      } else {
        throw err
      }
    }
  }

  /**
   * Edits a message with updated content
   * @arg {external:"Eris.Message"} message The message to edit
   * @arg {String} content The content to edit the message with
   * @arg {Object} [options] Options object
   * @arg {String} [options.lang] Locale of content
   * @arg {Number} [options.delay] Delay to edit the message
   * @returns {Promise}
   */
  edit (msg, content, options) {
    const { lang, delay = 0 } = options

    if (Array.isArray(content)) content = content.join('\n')
    if (this.i18n) content = this.t(content, lang, options)

    return (delay ? promDelay(delay) : Promise.resolve()).then(() => msg.edit(content))
  }

  /**
   * Deletes multiple messages if there are sufficient permissions
   * @arg {...external:"Eris.Message"} messages The messages to delete
   * @returns {Promise}
   */
  deleteMessages (...msgs) {
    const id = this._client.user.id
    return Promise.all(msgs.reduce((arr, msg) => {
      if (!msg || !msg.channel.guild) return arr
      if (msg.author.id === id || this.hasPermissions(msg.channel, msg.member, 'manageMessages')) {
        arr.push(msg.delete())
      }
      return arr
    }, []))
  }
}

module.exports = Base
<|endoftext|>


<commit_before>import React from 'react'
const shell = window.require('electron').shell
const clipboard = window.require('electron').clipboard
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'

// import necessary components
import {Card, CardActions, CardHeader, CardText} from 'material-ui/Card'
import FloatingActionButton from 'material-ui/FloatingActionButton'
import Tooltip from 'material-ui/internal/Tooltip'
import FlatButton from 'material-ui/FlatButton'
import IconButton from 'material-ui/IconButton'
import TextField from 'material-ui/TextField'
import Checkbox from 'material-ui/Checkbox'
import Dialog from 'material-ui/Dialog'

// icons
import ContentAdd from 'material-ui/svg-icons/content/add'
import Info from 'material-ui/svg-icons/action/info'

import mrEmitter from '../helpers/mrEmitter'

// remove this subscription afterwards when there is no use for it
let Subscription = null

// standard regex for matching the urls
// see http://www.regexr.com/3ajfi
const urlPattern = /([--:\w?@%&+~#=]*\.[a-z]{2,4}\/{0,2})((?:[?&](?:\w+)=(?:\w+))+|[--:\w?@%&+~#=]+)?/

export default class Addurl extends React.Component {
  //keep tooltip state
  state = {
    authentication: false,
    tooltipShown: false,
    dialog: false,
    fab: true
  }

  // click to open the Dialog
  openDownloadDialog = () => {
    this.setState({dialog: true})
    // focus the url input
    setTimeout(() => {
      this.refs.urlInput.focus()
      // check if the clipboard has a url (if yes paste it)
      if (urlPattern.test(clipboard.readText(String))) {

      }
    }, 300)
  }

  // close the dialog
  closeDownloadDialog = () => {
    this.setState({dialog: false})
  }

  // on info button click
  onInfoButton = () => {
    shell.openExternal('https://github.com/rg3/youtube-dl/blob/master/docs/supportedsites.md')
  }

  // authentication checkbox
  onAuthCheck = () => {
    this.setState({
      authentication: !this.state.authentication
    })
  }

  // handle expansion panel or the card
  onAuthenticationCheck = (check) => {
    this.setState({authentication: check})
  }

  // check if the page should have the fab or no
  isActive = (to) => {
    let newState
    if (to === '/' || to === '/downloading' || to === '/downloaded') {
      newState = true
    }
    else {
      newState = false
    }
    // don't update state if already the right one
    if (newState !== this.state.fab) {
      this.setState({fab: newState})
    }
  }

  // register all adding stuff here
  componentWillMount() {
    // on initiate load
    this.isActive(this.context.location.pathname)
    // on each event trigger
    Subscription = mrEmitter.addListener('onRouteChange', (newLocation) => {
      this.isActive(newLocation)
    });
  }

  // unregister all references here
  componentWillUnmount() {
    Subscription.remove()
  }

  render() {
    const style = {
    fab: {
      position: 'fixed',
      bottom: '25px',
      right: '20px',
      visibility: this.state.fab ? 'visible' : 'collapse',
      zIndex: 2
    },
    tooltip: {
      position: 'fixed',
      top: 'calc(100% - 100px)',
      right: '15px'
    },
    dialogTitle: {
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'space-between'
    },
    urlInput: {
      width: '100%'
    },
    authCover: {
      display: 'flex'
    },
    authUser: {
      marginRight: '1em',
      flex: 1
    },
    authPass: {
      flex: 1
    }
  }

    // dialog actions
  const actions = [
    <FlatButton
      label="Cancel"
      primary={true}
      onTouchTap={this.closeDownloadDialog}
    />,
    <FlatButton
      label="Ok"
      primary={true}
      onTouchTap={this.closeDownloadDialog}
    />
  ]

  return (

      <ReactCSSTransitionGroup
        transitionName="downloadedAnimate"
        transitionEnterTimeout={200}
        transitionLeaveTimeout={200}
        transitionAppear={true}
        transitionAppearTimeout={200}
      >
        <FloatingActionButton
          secondary={true}
          key={this.state.fab}
          style={style.fab}
          onTouchTap={this.openDownloadDialog}
          onMouseEnter={()=>{this.setState({tooltipShown: true})}}
          onMouseLeave={()=>{this.setState({tooltipShown: false})}}
        >
          <ContentAdd />
        </FloatingActionButton>
      </ReactCSSTransitionGroup>
      <Tooltip
        show={this.state.tooltipShown}
        label={"Download from a new URL"}
        style={style.tooltip}
        horizontalPosition="left"
        verticalPosition="top"
        touch={true}
      />
      <Dialog
        title={

              Download a video or a playlist (if supported)

            <IconButton
              tooltip="More Information regarding list of supported formats and websites"
              onTouchTap={this.onInfoButton}
            >
              <Info />
            </IconButton>

        }
        actions={actions}
        open={this.state.dialog}
        onRequestClose={this.closeDownloadDialog}
      >
        <TextField
          ref="urlInput"
          style={style.urlInput}
          hintText="e.g. https://www.youtube.com/watch?v=foE1mO2yM04"
          floatingLabelText="Enter or Paste the video url here"
        />
        <Card
          expanded={this.state.authentication}
          onExpandChange={this.onAuthenticationCheck}
        >
          <CardText>
            <Checkbox
              onCheck={this.onAuthCheck}
              checked={this.state.authentication}
              label="Enable Authentication"
            />
          </CardText>
          <CardText
            expandable={true}
            style={style.authCover}
          >
            <TextField
              ref="username"
              style={style.authUser}
              floatingLabelText="Username"
            />
            <TextField
              ref="password"
              floatingLabelText="Password"
              style={style.authPass}
              type="password"
            />
          </CardText>
        </Card>
      </Dialog>

    )
  }
}

Addurl.contextTypes = {
  location: React.PropTypes.object.isRequired
}
<commit_msg>Delete addurl.js<commit_after><|endoftext|>


<commit_before>System.config({
	transpiler: 'typescript',
	typescriptOptions: { emitDecoratorMetadata: true },
	map: {
		'app' : 'app',
		'rxjs': 'components/npm/rxjs',
		'@angular'                         : 'components/npm/@angular',
		'react'							   : 'components/bower/react',
		'underscore'					   : 'components/bower/underscore'
	},
	packages: {
		'app'                              : { main: 'main.js', defaultExtension: 'js', format: 'register' },
		'rxjs'                             : { main: 'index.js' },
		'@angular/core'                    : { main: 'index.js' },
		'@angular/common'                  : { main: 'index.js' },
		'@angular/compiler'                : { main: 'index.js' },
		'@angular/http'                    : { main: 'index.js' },
		'@angular/forms'				   : { main: 'index.js' },
		'@angular/router'                  : { main: 'index.js' },
		'@angular/platform-browser'        : { main: 'index.js' },
		'@angular/platform-browser-dynamic': { main: 'index.js' },
		'react'							   : { main: 'react.js' },
		'underscore'					   : { main: 'underscore.js' }
	}
});
<commit_msg>included redux in systemjs config<commit_after>System.config({
	transpiler: 'typescript',
	typescriptOptions: { emitDecoratorMetadata: true },
	map: {
		'app' : 'app',
		'rxjs': 'components/npm/rxjs',
		'@angular'                         : 'components/npm/@angular',
		'react'							   : 'components/bower/react',
		'underscore'					   : 'components/bower/underscore',
		'redux'							   : 'components/npm/redux/redux.min.js'
	},
	packages: {
		'app'                              : { main: 'main.js', defaultExtension: 'js', format: 'register' },
		'rxjs'                             : { main: 'index.js' },
		'@angular/core'                    : { main: 'index.js' },
		'@angular/common'                  : { main: 'index.js' },
		'@angular/compiler'                : { main: 'index.js' },
		'@angular/http'                    : { main: 'index.js' },
		'@angular/forms'				   : { main: 'index.js' },
		'@angular/router'                  : { main: 'index.js' },
		'@angular/platform-browser'        : { main: 'index.js' },
		'@angular/platform-browser-dynamic': { main: 'index.js' },
		'react'							   : { main: 'react.js' },
		'underscore'					   : { main: 'underscore.js' }
	}
});
<|endoftext|>


<commit_before>
const defaultOptions = {
  virtualPageViewEvent: 'virtual_pageview',
  startPush: {},
  addExperiments: [],
};

export default class TagManager {
  constructor(dataLayer, params = defaultOptions) {
    this.options = Object.assign(defaultOptions, params);
    this.dataLayer = dataLayer || [];
    this.addExperiments();
    this.dataLayer.push(this.options.startPush);
  }

  addExperiments() {
    Object.assign(this.options.startPush, {
      experiments: this.experiments(),
    });

    return this;
  }

  experiments() {
    return (this.options.startPush.experiments || []).concat(this.options.addExperiments);
  }

  prependExperiment(experiment) {
    if (this.dataLayer[0].experiments) {
      const dataLayer = Object.assign({}, this.options.startPush, {
        experiments: [experiment],
      });

      this.dataLayer.unshift(dataLayer);
      return this.dataLayer[0].experiments;
    }

    return this.dataLayer[0].experiments.push(experiment);
  }

  virtualPageView(vpname, event) {
    this.dataLayer.push({
      vpname,
      event: event || this.options.virtualPageViewEvent,
    });
  }

  eventCategory(event, infos) {
    const categoryObj = Object.assign({
      eventCategory: event,
      event: 'GAEvent',
    }, infos || {});

    return this.dataLayer.push(categoryObj);
  }

  custom(obj) {
    return this.dataLayer.push(obj);
  }

  bindEvents($) {
    if (!$) throw new Error('jQuery must be passed here');

    $('[data-gtm-event=\'ga-event\']').on('click.ga-event', this.clickGAEvent.bind(this));
  }

  clickGAEvent(event) {
    const el = event.curentTarget;

    const category = el.getAttribute('data-gtm-category');
    const eventAction = el.getAttribute('data-gtm-action') || null;
    const eventLabel = el.getAttribute('data-gtm-label') || null;
    const eventValue = el.getAttribute('data-gtm-value') || null;
    const eventProperty = el.getAttribute('data-gtm-property') || null;

    this.eventCategory(category, { eventAction, eventLabel, eventValue, eventProperty });
  }
}
<commit_msg>Adding key to default option from constructor<commit_after>
const defaultOptions = {
  virtualPageViewEvent: 'virtual_pageview',
  startPush: {
    experiements: [],
  },
  addExperiments: [],
};

export default class TagManager {
  constructor(dataLayer, params = defaultOptions) {
    this.options = Object.assign(defaultOptions, params);
    this.dataLayer = dataLayer || [];
    this.addExperiments();
    this.dataLayer.push(this.options.startPush);
  }

  addExperiments() {
    Object.assign(this.options.startPush, {
      experiments: this.experiments(),
    });

    return this;
  }

  experiments() {
    return this.options.startPush.experiments.concat(this.options.addExperiments);
  }

  prependExperiment(experiment) {
    if (this.dataLayer[0].experiments) {
      const dataLayer = Object.assign({}, this.options.startPush, {
        experiments: [experiment],
      });

      this.dataLayer.unshift(dataLayer);
      return this.dataLayer[0].experiments;
    }

    return this.dataLayer[0].experiments.push(experiment);
  }

  virtualPageView(vpname, event) {
    this.dataLayer.push({
      vpname,
      event: event || this.options.virtualPageViewEvent,
    });
  }

  eventCategory(event, infos) {
    const categoryObj = Object.assign({
      eventCategory: event,
      event: 'GAEvent',
    }, infos || {});

    return this.dataLayer.push(categoryObj);
  }

  custom(obj) {
    return this.dataLayer.push(obj);
  }

  bindEvents($) {
    if (!$) throw new Error('jQuery must be passed here');

    $('[data-gtm-event=\'ga-event\']').on('click.ga-event', this.clickGAEvent.bind(this));
  }

  clickGAEvent(event) {
    const el = event.curentTarget;

    const category = el.getAttribute('data-gtm-category');
    const eventAction = el.getAttribute('data-gtm-action') || null;
    const eventLabel = el.getAttribute('data-gtm-label') || null;
    const eventValue = el.getAttribute('data-gtm-value') || null;
    const eventProperty = el.getAttribute('data-gtm-property') || null;

    this.eventCategory(category, { eventAction, eventLabel, eventValue, eventProperty });
  }
}
<|endoftext|>


<commit_before>import Promise from 'bluebird';
import { create, exists } from '../../utils/file'
import fs from 'fs';
import exec from '../../exec.js';

const readFile = Promise.promisify(fs.readFile);

export const run = async ({ host, path, method }) => {
    if (method === 'apache') {
        try {
            await exec(`letsencrypt certonly -d ${host} --apache --quiet`);
        } catch (err) {
            throw new Error('Failed to create certificates using Apache Method')
        }
    } else if (method === 'dns') {
        try {
            let hookScript =__dirname + '/../../scripts/dnsAcmeHook.js'
            await exec(`certbot certonly --manual-public-ip-logging-ok --preferred-challenge=dns  --manual-auth-hook ${hookScript} --cert-name ${host} -d ${host} --manual --quiet`, null, null, { ...process.env });
        } catch (err) {
            throw new Error('Failed to create certificates using DNS Method')
        }

    }
    let lastmodifiedFolder = await getlastModifiedFolder(host)
    let letsEncryptCertsExist = await Promise.all([exists(`/etc/letsencrypt/live/${lastmodifiedFolder}/privkey.pem`), exists(`/etc/letsencrypt/live/${lastmodifiedFolder}/fullchain.pem`)])
    if (letsEncryptCertsExist.includes(false)) {
        throw new Error('CertBot Certs not created')
    } else {
        let fullchain = await readFile(`/etc/letsencrypt/live/${lastmodifiedFolder}/fullchain.pem`)
        let privkey = await readFile(`/etc/letsencrypt/live/${lastmodifiedFolder}/privkey.pem`)
        return Promise.all([create(path + '/certs/fullchain.pem', fullchain), create(path + '/certs/privkey.pem', privkey)])
    }
};

async function getlastModifiedFolder (domain) {
    let path = '/etc/letsencrypt/live'
    return fs.readdirSync(path).filter(function (file) {
        return file.startsWith(domain) && fs.statSync(path+'/'+file).isDirectory()
    }).reduce((last, current) => {
        let currentFileDate = new Date(fs.statSync(path+'/' + current).mtime);
        let lastFileDate = new Date(fs.statSync(path+'/' + last).mtime);
        return ( currentFileDate.getTime() > lastFileDate.getTime() ) ? current: last;
    })
}
<commit_msg>create symlink for file instad of copying file in order to make certbot renewal work<commit_after>import Promise from 'bluebird';
import {
    create,
    exists
} from '../../utils/file'
import fs from 'fs';
import exec from '../../exec.js';

const readFile = Promise.promisify(fs.readFile);

export const run = async ({
    host,
    path,
    method,
    hostAndAliases
}) => {
    if (method === 'apache') {
        try {
            await exec(`letsencrypt certonly -d ${hostAndAliases} --apache --quiet`);
        } catch (err) {
            throw new Error('Failed to create certificates using Apache Method')
        }
    } else if (method === 'dns') {
        try {
            let hookScript = __dirname + '/../../scripts/dnsAcmeHook.js'
            await exec(`certbot certonly --manual-public-ip-logging-ok --preferred-challenge=dns  --manual-auth-hook ${hookScript} -d ${hostAndAliases} --manual --quiet`, null, null, {
                ...process.env
            });
        } catch (err) {
            throw new Error('Failed to create certificates using DNS Method')
        }

    }
    let lastmodifiedFolder = await getlastModifiedFolder(host)
    let letsEncryptCertsExist = await Promise.all([exists(`/etc/letsencrypt/live/${lastmodifiedFolder}/privkey.pem`), exists(`/etc/letsencrypt/live/${lastmodifiedFolder}/fullchain.pem`)])
    if (letsEncryptCertsExist.includes(false)) {
        throw new Error('CertBot Certs not created')
    } else {
        let fullchain = await path.resolve(`/etc/letsencrypt/live/${lastmodifiedFolder}/fullchain.pem`)
        let privkey = await path.resolve(`/etc/letsencrypt/live/${lastmodifiedFolder}/privkey.pem`)
        return Promise.all(
            [
                exec('ln -s {{source}} {{destination}}', {
                    source: privkey,
                    destination: path.resolve(path + '/certs/privkey.pem')
                }),
                exec('ln -s {{source}} {{destination}}', {
                    source: fullchain,
                    destination: path.resolve(path + '/certs/fullchain.pem')
                }),
            ])
    }
};

async function getlastModifiedFolder(domain) {
    let path = '/etc/letsencrypt/live'
    return fs.readdirSync(path).filter(function(file) {
        return file.startsWith(domain) && fs.statSync(path + '/' + file).isDirectory()
    }).reduce((last, current) => {
        let currentFileDate = new Date(fs.statSync(path + '/' + current).mtime);
        let lastFileDate = new Date(fs.statSync(path + '/' + last).mtime);
        return (currentFileDate.getTime() > lastFileDate.getTime()) ? current : last;
    })
}
<|endoftext|>


<commit_before>define('lyria/template/string', function() {
  var templateString = {
    key: {
      start: '{{',
      end: '}}'
    },
    process: function(value, parameter) {
      if (value == null) {
        return;
      }

      if (parameter == null) {
        return value;
      }

      // Array or object
      if ( typeof parameter === 'object') {
        if (Array.isArray(parameter)) {
          for (var i = 0, j = parameter.length; i < j; i++) {
            value = value.replace(new RegExp(templateString.key.start + i + templateString.key.end), parameter[i]);
          }
        } else {
          var paramKeys = Object.keys(parameter);

          for (var k = 0, l = paramKeys.length; k < l; k++) {
            (function(item) {
              value = value.replace(new RegExp(templateString.key.start + paramKeys[k] + templateString.key.end), item);
            })(parameter[paramKeys[k]]);
          }
        }
      }

      return value;
    }
  };

  return templateString;
});
<commit_msg>Allowing duplicate keys in lyria/template/string<commit_after>define('lyria/template/string', function() {
  var templateString = {
    key: {
      start: '{{',
      end: '}}'
    },
    process: function(value, parameter) {
      if (value == null) {
        return;
      }

      if (parameter == null) {
        return value;
      }

      // Array or object
      if ( typeof parameter === 'object') {
        if (Array.isArray(parameter)) {
          for (var i = 0, j = parameter.length; i < j; i++) {
            value = value.replace(new RegExp(templateString.key.start + i + templateString.key.end, 'g'), parameter[i]);
          }
        } else {
          var paramKeys = Object.keys(parameter);

          for (var k = 0, l = paramKeys.length; k < l; k++) {
            (function(item) {
              value = value.replace(new RegExp(templateString.key.start + paramKeys[k] + templateString.key.end, 'g'), item);
            })(parameter[paramKeys[k]]);
          }
        }
      }

      return value;
    }
  };

  return templateString;
});
<|endoftext|>


<commit_before>
// Regex expression shortcuts
var pr_amp = /&/g;
var pr_lt = /</g;
var pr_gt = />/g;
var pr_quot = /\"/g;

/**
* Escape html special characters.
*/
function textToHtml(str) {
    return str.replace(pr_amp, '&')
        .replace(pr_lt, '<')
        .replace(pr_gt, '>');
}

function attribToHtml(str) {
    return str.replace(pr_amp, '&')
        .replace(pr_lt, '<')
        .replace(pr_gt, '>')
        .replace(pr_quot, '"');
}

/**
* Order HTML attributes for consistent HTML comparison.
*
* Adapted from google-code-prettify
* http://code.google.com/p/google-code-prettify/source/browse/trunk/src/prettify.js#311
* Apache license, Copyright (C) 2006 Google Inc.
*/
function normalizeHtml(node) {
    var html = '',
        i;
    switch (node.nodeType) {
    case 1:  // an element
        var name = node.tagName.toLowerCase(),
            attrs,
            attr,
            n,
            child,
            sortedAttrs;

        html += '<' + name;
        attrs = node.attributes;
        n = attrs.length;
        if (n) {
            // Node has attributes, order them
            sortedAttrs = [];
            for (i = n; --i >= 0;) {
                attr = attrs[i];
                if (attr.specified) {
                    // We only care about specified attributes
                    sortedAttrs.push(attr);
                }
            }
            sortedAttrs.sort(function (a, b) {
                return (a.name < b.name) ? -1 : a.name === b.name ? 0 : 1;
            });
            attrs = sortedAttrs;

            for (i = 0; i < attrs.length; ++i) {
                attr = attrs[i];
                html += ' ' + attr.name.toLowerCase() +
                    '="' + attribToHtml(attr.value) + '"';
            }
        }
        html += '>';
        for (child = node.firstChild; child; child = child.nextSibling) {
            html += normalizeHtml(child);
        }
        if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
            html += '<\/' + name + '>';
        }

        break;
    case 3:
    case 4: // text
        html += textToHtml(node.nodeValue);
        break;
    }

    return html;
}

/**
* Ensure the cleaned xhtml coming from a WYMeditor instance matches the
* expected HTML, accounting for differing whitespace and attribute ordering.
*/
function htmlEquals(wymeditor, expected) {
    var xhtml = jQuery.trim(wymeditor.xhtml());
    if (xhtml === '') {
        // In jQuery 1.2.x, $('') returns an empty list, so we can't call
        // normalizeHTML. On 1.3.x or higher upgrade, we can remove this
        // check for the empty string
        equals(xhtml, expected);
        return;
    }

    var normedActual = normalizeHtml($(xhtml)[0]);
    var normedExpected = normalizeHtml($(expected)[0]);
    equals(normedActual, normedExpected);
}

function makeSelection(wymeditor, startElement, endElement, startElementIndex, endElementIndex) {
    if (startElementIndex === null) {
        startElementIndex = 0;
    }
    if (endElementIndex === null) {
        endElementIndex = 0;
    }
    var sel = rangy.getIframeSelection(wymeditor._iframe);

    var range = rangy.createRange(wymeditor._doc);
    range.setStart(startElement, startElementIndex);
    range.setEnd(endElement, endElementIndex);
    if (startElement === endElement &&
            startElementIndex === 0 && endElementIndex === 0) {
        // Only collapse the range if we're selecting the start of one element
        range.collapse(false);
    }

    try {
        sel.setSingleRange(range);
    } catch (err) {
        // ie8 can raise an "unkown runtime error" trying to empty the range
    }
    // IE selection hack
    if ($.browser.msie) {
        wymeditor.saveCaret();
    }
}

/*
* Move the selection to the start of the given element within the editor.
*/
function moveSelector(wymeditor, selectedElement) {
    makeSelection(wymeditor, selectedElement, selectedElement, 0, 0);

    equals(wymeditor.selected(), selectedElement, "moveSelector");
}

/*
    Simulate a keypress, firing off the keydown, keypress and keyup events.
*/
function simulateKey(keyCode, targetElement, options) {
    var defaults = {
        'metaKey': false,
        'ctrlKey': false,
        'shiftKey': false,
        'altKey': false
    };

    options = $.extend(defaults, options);

    var keydown = $.Event('keydown');
    keydown.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    var keypress = $.Event('keypress');
    keypress.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    var keyup = $.Event('keyup');
    keyup.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    $(targetElement).trigger(keydown);
    $(targetElement).trigger(keypress);
    $(targetElement).trigger(keyup);
}

/*
    Determine if this element is editable.
    Mimics https://developer.mozilla.org/en/DOM/element.isContentEditable
*/
function isContentEditable(element) {
    // We can't use isContentEditable in firefox 7 because it doesn't take
    // in to account designMode like firefox 3 did
    if (!$.browser.mozilla && typeof element.isContentEditable  !== 'undefined') {
        return element.isContentEditable;
    }

    if (element.contentEditable === '' || element.contentEditable === true) {
        return true;
    } else if (element.contentEditable === false) {
        return false;
    } else {
        if (element.parentNode === null) {
            if (element.designMode === "on") {
                return true;
            }
            return false;
        }
        return isContentEditable(element.parentNode);
    }
}
<commit_msg>Refs #309. Modified the htmlEquals testing utility so that it works on sibling top-level elements<commit_after>
// Regex expression shortcuts
var pr_amp = /&/g;
var pr_lt = /</g;
var pr_gt = />/g;
var pr_quot = /\"/g;

/**
* Escape html special characters.
*/
function textToHtml(str) {
    return str.replace(pr_amp, '&')
        .replace(pr_lt, '<')
        .replace(pr_gt, '>');
}

function attribToHtml(str) {
    return str.replace(pr_amp, '&')
        .replace(pr_lt, '<')
        .replace(pr_gt, '>')
        .replace(pr_quot, '"');
}

/**
* Order HTML attributes for consistent HTML comparison.
*
* Adapted from google-code-prettify
* http://code.google.com/p/google-code-prettify/source/browse/trunk/src/prettify.js#311
* Apache license, Copyright (C) 2006 Google Inc.
*/
function normalizeHtml(node) {
    var html = '',
        i;
    switch (node.nodeType) {
    case 1:  // an element
        var name = node.tagName.toLowerCase(),
            attrs,
            attr,
            n,
            child,
            sortedAttrs;

        html += '<' + name;
        attrs = node.attributes;
        n = attrs.length;
        if (n) {
            // Node has attributes, order them
            sortedAttrs = [];
            for (i = n; --i >= 0;) {
                attr = attrs[i];
                if (attr.specified) {
                    // We only care about specified attributes
                    sortedAttrs.push(attr);
                }
            }
            sortedAttrs.sort(function (a, b) {
                return (a.name < b.name) ? -1 : a.name === b.name ? 0 : 1;
            });
            attrs = sortedAttrs;

            for (i = 0; i < attrs.length; ++i) {
                attr = attrs[i];
                html += ' ' + attr.name.toLowerCase() +
                    '="' + attribToHtml(attr.value) + '"';
            }
        }
        html += '>';
        for (child = node.firstChild; child; child = child.nextSibling) {
            html += normalizeHtml(child);
        }
        if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
            html += '<\/' + name + '>';
        }

        break;
    case 3:
    case 4: // text
        html += textToHtml(node.nodeValue);
        break;
    }

    return html;
}

/**
* Ensure the cleaned xhtml coming from a WYMeditor instance matches the
* expected HTML, accounting for differing whitespace and attribute ordering.
*/
function htmlEquals(wymeditor, expected) {
    var xhtml = '',
        normedActual = '',
        normedExpected = '',
        tmpNodes,
        i;
    xhtml = jQuery.trim(wymeditor.xhtml());
    if (xhtml === '') {
        // In jQuery 1.2.x, $('') returns an empty list, so we can't call
        // normalizeHTML. On 1.3.x or higher upgrade, we can remove this
        // check for the empty string
        equals(xhtml, expected);
        return;
    }

    tmpNodes = $(xhtml, wymeditor._doc);
    for (i = 0; i < tmpNodes.length; i++) {
        normedActual += normalizeHtml(tmpNodes[i]);
    }
    tmpNodes = $(expected, wymeditor._doc);
    for (i = 0; i < tmpNodes.length; i++) {
        normedExpected += normalizeHtml(tmpNodes[i]);
    }

    equals(normedActual, normedExpected);
}

function makeSelection(wymeditor, startElement, endElement, startElementIndex, endElementIndex) {
    if (startElementIndex === null) {
        startElementIndex = 0;
    }
    if (endElementIndex === null) {
        endElementIndex = 0;
    }
    var sel = rangy.getIframeSelection(wymeditor._iframe);

    var range = rangy.createRange(wymeditor._doc);
    range.setStart(startElement, startElementIndex);
    range.setEnd(endElement, endElementIndex);
    if (startElement === endElement &&
            startElementIndex === 0 && endElementIndex === 0) {
        // Only collapse the range if we're selecting the start of one element
        range.collapse(false);
    }

    try {
        sel.setSingleRange(range);
    } catch (err) {
        // ie8 can raise an "unkown runtime error" trying to empty the range
    }
    // IE selection hack
    if ($.browser.msie) {
        wymeditor.saveCaret();
    }
}

/*
* Move the selection to the start of the given element within the editor.
*/
function moveSelector(wymeditor, selectedElement) {
    makeSelection(wymeditor, selectedElement, selectedElement, 0, 0);

    equals(wymeditor.selected(), selectedElement, "moveSelector");
}

/*
    Simulate a keypress, firing off the keydown, keypress and keyup events.
*/
function simulateKey(keyCode, targetElement, options) {
    var defaults = {
        'metaKey': false,
        'ctrlKey': false,
        'shiftKey': false,
        'altKey': false
    };

    options = $.extend(defaults, options);

    var keydown = $.Event('keydown');
    keydown.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    var keypress = $.Event('keypress');
    keypress.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    var keyup = $.Event('keyup');
    keyup.keyCode = keyCode;
    keydown.metaKey = options.metaKey;
    keydown.ctrlKey = options.ctrlKey;
    keydown.shiftKey = options.shiftKey;
    keydown.altKey = options.altKey;

    $(targetElement).trigger(keydown);
    $(targetElement).trigger(keypress);
    $(targetElement).trigger(keyup);
}

/*
    Determine if this element is editable.
    Mimics https://developer.mozilla.org/en/DOM/element.isContentEditable
*/
function isContentEditable(element) {
    // We can't use isContentEditable in firefox 7 because it doesn't take
    // in to account designMode like firefox 3 did
    if (!$.browser.mozilla && typeof element.isContentEditable  !== 'undefined') {
        return element.isContentEditable;
    }

    if (element.contentEditable === '' || element.contentEditable === true) {
        return true;
    } else if (element.contentEditable === false) {
        return false;
    } else {
        if (element.parentNode === null) {
            if (element.designMode === "on") {
                return true;
            }
            return false;
        }
        return isContentEditable(element.parentNode);
    }
}
<|endoftext|>