<commit_before>'use strict';

var crypto = require('crypto');
var _ = require('underscore');
var mongoose = require('mongoose');
var semver = require('semver');
var stableStringify = require('json-stable-stringify');
var async = require('async');
var dotty = require('dotty');
var bufferEqual = require('buffer-equal-constant-time');
var mpath = require('mpath');

var objectUtil = require('../util/object-util.js');
var cryptoUtil = require('../util/crypto-util.js');

var pick = objectUtil.pick;
var setFieldValue = objectUtil.setFieldValue;
var isEmbeddedDocument = objectUtil.isEmbeddedDocument;
var drop256 = cryptoUtil.drop256;
var deriveKey = cryptoUtil.deriveKey;

/**  Plugin Constants */

var VERSION = 'a';

var ENCRYPTION_ALGORITHM = 'aes-256-cbc';
var IV_LENGTH = 16;
var AAC_LENGTH = 32;
var VERSION_LENGTH = 1;
var VERSION_BUF = new Buffer(VERSION);

/**
 * Check For Compatible Mongoose Version
 */
if(semver.gt(process.version, '4.0.0')){
    if(semver.lt(mongoose.version, '4.2.4')){
        throw new Error('Mongoose version 4.2.4 or greater is required for Node version 4.0.0 or greater');
    }
}

const isMongoose5 = semver.gte(mongoose.version, '5.0.0')

/**
 * Mongoose encryption plugin
 * @module mongoose-encryption
 *
 *
 * @param      {Object}     schema   The schema
 * @param      {Object}     options  Plugin options
 * @param      {string}     [options.secret]  A secret string which will be used to generate an encryption key and a signing key
 * @param      {string}     [options.encryptionKey]  A secret string which will be used to generate an encryption key
 * @param      {string}     [options.signingKey]  A secret string which will be used to generate a signing key
 * @param      {string[]}   [options.encryptedFields]  A list of fields to encrypt. Default is to encrypt all fields.
 * @param      {string[]}   [options.excludeFromEncryption]  A list of fields to not encrypt
 * @param      {string[]}   [options.additionalAuthenticatedFields]  A list of fields to authenticate even if they aren't encrypted
 * @param      {boolean}    [options.requireAuthenticationCode=true]  Whether documents without an authentication code are valid
 * @param      {boolean}    [options.decryptPostSave=true]  Whether to automatically decrypt documents in the application after saving them (faster if false)
 * @param      {string}     [options.collectionId]  If you update the Model name of the schema, this should be set to its original name
 * @return     {undefined}
 */

var mongooseEncryption = function(schema, options) {
    var encryptedFields, excludedFields, authenticatedFields, encryptionKey, signingKey, path;

    _.defaults(options, {
        middleware: true, // allow for skipping middleware with false
        requireAuthenticationCode: true, // allow for no authentication code on docs (not recommended),
        decryptPostSave: true, // allow for skipping the decryption after save for improved performance
        _suppressDuplicatePluginError: false // used for testing only

    });

    /** Encryption Keys */

    if (options.secret) {
        if (options.encryptionKey || options.signingKey) {
            throw new Error('if options.secret is used, then options.encryptionKey and options.signingKey must not be included');
        } else {
            encryptionKey = drop256(deriveKey(options.secret, 'enc'));
            signingKey = deriveKey(options.secret, 'sig');
        }
    } else {
        if (!options.encryptionKey || !options.signingKey) {
            throw new Error('must provide either options.secret or both options.encryptionKey and options.signingKey');
        } else {
            encryptionKey = new Buffer(options.encryptionKey, 'base64');
            if (encryptionKey.length !== 32) {
                throw new Error('options.encryptionKey must be a a 32 byte base64 string');
            }
            signingKey = new Buffer(options.signingKey, 'base64');
            if (signingKey.length !== 64) {
                throw new Error('options.signingKey must be a a 64 byte base64 string');
            }
        }
    }

    /** Deprecated options */

    if (options.fields) {
        options.encryptedFields = options.fields;
        console.warn('options.fields has been deprecated. please use options.encryptedFields');
    }
    if (options.exclude) {
        options.excludeFromEncryption = options.exclude;
        console.warn('options.fields has been deprecated. please use options.excludeFromEncryption');
    }

    /** Encryption Options */

    if (options.encryptedFields) {
        encryptedFields = _.difference(options.encryptedFields, ['_ct']);
    } else {
        excludedFields = _.union(['_id', '_ct'], options.excludeFromEncryption);
        encryptedFields = _.chain(schema.paths)
            .filter(function(pathDetails) { // exclude indexed fields
                return !pathDetails._index
            })
            .pluck('path') // get path name
            .difference(excludedFields) // exclude excluded fields
            .uniq()
            .value();
    }

    /** Authentication Options */

    var baselineAuthenticateFields = ['_id', '_ct'];

    if (options.additionalAuthenticatedFields) {
        authenticatedFields = _.union(options.additionalAuthenticatedFields, baselineAuthenticateFields);
    } else {
        authenticatedFields = baselineAuthenticateFields;
    }

    /** Augment Schema */

    if (!schema.paths._ct) { // ciphertext
        schema.add({
            _ct: {
                type: Buffer
            }
        });
    }
    if (!schema.paths._ac) { // authentication code
        schema.add({
            _ac: {
                type: Buffer
            }
        });
    }

    /** Authentication Functions */

    var computeAC = function(doc, fields, version, modelName) {
        // HMAC-SHA512-drop-256
        var hmac = crypto.createHmac('sha512', signingKey);

        if (!(fields instanceof Array)){
            throw new Error('fields must be an array');
        }
        if (fields.indexOf('_id') === -1) {
            throw new Error('_id must be in array of fields to authenticate');
        }
        if (fields.indexOf('_ac') !== -1) {
            throw new Error('_ac cannot be in array of fields to authenticate');
        }

        var collectionId = options.collectionId || modelName || doc.constructor.modelName;

        if (!collectionId) {
            throw new Error('For authentication, each collection must have a unique id. This is normally the model name when there is one, but can be overridden or added by options.collectionId');
        }

        // convert to regular object if possible in order to convert to the eventual mongo form which may be different than mongoose form
        // and only pick fields that will be authenticated
        var objectToAuthenticate = pick((doc.toObject ? doc.toObject() : doc), fields);
        var stringToAuthenticate = stableStringify(objectToAuthenticate);
        hmac.update(collectionId);
        hmac.update(version);
        hmac.update(stringToAuthenticate);
        hmac.update(JSON.stringify(fields));
        var fullAuthenticationBuffer = new Buffer(hmac.digest());
        return drop256(fullAuthenticationBuffer);
    };

    /** Functions To Check If Authenticated Fields Were Selected By Query */

    var authenticationFieldsToCheck = _.chain(authenticatedFields).union(['_ac']).without('_id').value(); // _id is implicitly selected

    var authenticatedFieldsIsSelected = function(doc){
        return _.map(authenticationFieldsToCheck, function(field) {
            return doc.isSelected(field);
        });
    };

    var allAuthenticationFieldsSelected = function(doc){
        var isSelected = authenticatedFieldsIsSelected(doc);
        if (_.uniq(isSelected).length === 1){
            return isSelected[0];
        } else {
            return false;
        }
    };

    var noAuthenticationFieldsSelected = function(doc){
        var isSelected = authenticatedFieldsIsSelected(doc);
        if (_.uniq(isSelected).length === 1){
            return isSelected[0] === false;
        } else {
            return false;
        }
    };

    /** Ensure plugin only added once per schema */
    if(schema.statics._mongooseEncryptionInstalled){
        if(!options._suppressDuplicatePluginError){
            throw new Error(
                'Mongoose encryption plugin can only be added once per schema.\n\n' +
                'If you are running migrations, please remove encryption middleware first. ' +
                'Migrations should be run in a script where `encrypt.migrations` is added to the schema, '+
                'however the standard `encrypt` middleware should not be present at the same time. '
            );
        }

    } else {
        schema.statics._mongooseEncryptionInstalled = true;
    }

    /** Middleware */

    if (options.middleware) { // defaults to true
        schema.pre('init', function(next, data) {
            if (isMongoose5) {
                // 5.x does not provide a 'next' function for this middleware since it's synchronous
                data = next
            }
            var err = null;
            try { // this hook must be synchronous for embedded docs, so everything is synchronous for code simplicity
                if (!isEmbeddedDocument(this)){ // don't authenticate embedded docs because there's no way to handle the error appropriately
                    if (allAuthenticationFieldsSelected(this)) {
                        this.authenticateSync.call(data, this.constructor.modelName);
                    } else {
                        if (!noAuthenticationFieldsSelected(this)){
                            throw new Error("Authentication failed: Only some authenticated fields were selected by the query. Either all or none of the authenticated fields (" + authenticationFieldsToCheck + ") should be selected for proper authentication.");
                        }
                    }
                }
                if (this.isSelected('_ct')){
                    this.decryptSync.call(data);
                }
            } catch (e) {
                err = e;
            }

            if (isEmbeddedDocument(this)) {
                if (err) {
                    console.error(err);
                    throw err; // note: this won't actually get thrown until save, because errors in subdoc init fns are CastErrors and aren't thrown by validate()
                }
                this._doc = data;
                if (!isMongoose5) {
                    next();
                }
                return this;
            } else {
                if (!isMongoose5) {
                    return next(err);
                }
            }
        });

        schema.pre('save', function(next) {
            var that = this;
            if (this.isNew || this.isSelected('_ct') ){
                that.encrypt(function(err){
                    if (err) {
                        next(err);
                    } else {
                        if ((that.isNew || allAuthenticationFieldsSelected(that)) && !isEmbeddedDocument(that)) {
                            _.forEach(authenticatedFields, function(authenticatedField){
                                that.markModified(authenticatedField)
                            });

                            that.sign(next);
                        } else {
                            next();
                        }
                    }
                });
            } else if (allAuthenticationFieldsSelected(this) && !isEmbeddedDocument(this)) { // _ct is not selected but all authenticated fields are. cannot get hit in current version.
                _.forEach(authenticatedFields, function(authenticatedField){
                    that.markModified(authenticatedField)
                });

                this.sign(next);
            } else {
                next();
            }
        });

        if (options.decryptPostSave) { // true by default
            schema.post('save', function(doc) {
                if (_.isFunction(doc.decryptSync)) {
                    doc.decryptSync();
                }

                // Until 3.8.6, Mongoose didn't trigger post save hook on EmbeddedDocuments,
                // instead had to call decrypt on all subDocs.
                // ref https://github.com/LearnBoost/mongoose/issues/915

                doc._decryptEmbeddedDocs();

                return doc;
            });
        }
    }

    /** Encryption Instance Methods */

    schema.methods.encrypt = function(cb) {
        var that = this;

        if(this._ct){
            return cb(new Error('Encrypt failed: document already contains ciphertext'));
        }

        // generate random iv
        crypto.randomBytes(IV_LENGTH, function(err, iv) {
            var cipher, jsonToEncrypt, objectToEncrypt;
            if (err) {
                return cb(err);
            }
            cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, encryptionKey, iv);
            objectToEncrypt = pick(that, encryptedFields, {excludeUndefinedValues: true});

            jsonToEncrypt = JSON.stringify(objectToEncrypt);

            cipher.end(jsonToEncrypt, 'utf-8', function() {
                // add ciphertext to document
                that._ct = Buffer.concat([VERSION_BUF, iv, cipher.read()]);

                // remove encrypted fields from cleartext
                encryptedFields.forEach(function(field){
                    setFieldValue(that, field, undefined);
                });

                cb(null);
            });
        });
    };

    schema.methods.decrypt = function(cb) { // callback style but actually synchronous to allow for decryptSync without copypasta or complication
        try {
            schema.methods.decryptSync.call(this);
        } catch(e){
            return cb(e);
        }
        cb();
    };

    schema.methods.decryptSync = function() {
        var that = this;
        var ct, ctWithIV, decipher, iv, idString, decryptedObject, decryptedObjectJSON, decipheredVal;
        if (this._ct) {
            ctWithIV = this._ct.hasOwnProperty('buffer') ? this._ct.buffer : this._ct;
            iv = ctWithIV.slice(VERSION_LENGTH, VERSION_LENGTH + IV_LENGTH);
            ct = ctWithIV.slice(VERSION_LENGTH + IV_LENGTH, ctWithIV.length);

            decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, encryptionKey, iv);
            try {
                decryptedObjectJSON = decipher.update(ct, undefined, 'utf8') + decipher.final('utf8');
                decryptedObject = JSON.parse(decryptedObjectJSON);
            } catch (err) {
                if (this._id) {
                    idString = this._id.toString();
                } else {
                    idString = 'unknown';
                }
                throw new Error('Error parsing JSON during decrypt of ' + idString + ': ' + err);
            }

            encryptedFields.forEach(function(field) {
                decipheredVal = mpath.get(field, decryptedObject);

                //JSON.parse returns {type: "Buffer", data: Buffer} for Buffers
                //https://nodejs.org/api/buffer.html#buffer_buf_tojson
                if(_.isObject(decipheredVal) && decipheredVal.type === "Buffer"){
                    setFieldValue(that, field, decipheredVal.data);
                }else {
                    setFieldValue(that, field, decipheredVal);
                }
            });

            this._ct = undefined;
            this._ac = undefined;
        }
    };

    /**
     * Decrypt any embedded documents inside of this document
     *
     * @private
     * Should not be needed outside plugin
     */
    schema.methods._decryptEmbeddedDocs = function () {
        _.keys(this.schema.paths).forEach(function (path) {
            if (path === '_id' || path === '__v') {
                return;
            }

            var nestedDoc = dotty.get(this, path);

            if (nestedDoc && nestedDoc[0] && isEmbeddedDocument(nestedDoc[0])) {
                nestedDoc.forEach(function (subDoc) {
                    if (_.isFunction(subDoc.decryptSync)) {
                        subDoc.decryptSync();
                    }
                });
            }
        });
    };

    /** Authentication Instance Methods */

    schema.methods.sign = function(cb) {
        var basicAC = computeAC(this, authenticatedFields, VERSION);
        var authenticatedFieldsBuf = new Buffer(JSON.stringify(authenticatedFields));
        this._ac = Buffer.concat([VERSION_BUF, basicAC, authenticatedFieldsBuf]);
        cb();
    };

    schema.methods.authenticate = function(cb) { // callback style but actually synchronous to allow for decryptSync without copypasta or complication
        try {
            schema.methods.authenticateSync.call(this);
        } catch(e){
            return cb(e);
        }
        cb();
    };

    schema.methods.authenticateSync = function() {
        if (!this._ac) {
            if (options.requireAuthenticationCode) {
                throw new Error('Authentication code missing');
            } else {
                return null;
            }
        }
        var acBuf = this._ac.hasOwnProperty('buffer') ? this._ac.buffer : this._ac;
        if (acBuf.length < VERSION_LENGTH + AAC_LENGTH + 2) {
            throw new Error('_ac is too short and has likely been cut off or modified');
        }
        var versionUsed = acBuf.slice(0, VERSION_LENGTH).toString();
        var basicAC = acBuf.slice(VERSION_LENGTH, VERSION_LENGTH + AAC_LENGTH);
        var authenticatedFieldsUsed = JSON.parse(acBuf.slice(VERSION_LENGTH + AAC_LENGTH, acBuf.length).toString());

        var expectedHMAC = computeAC(this, authenticatedFieldsUsed, versionUsed, arguments[0]); // pass in modelName as argument in init hook

        var authentic = bufferEqual(basicAC, expectedHMAC);
        if (!authentic){
            throw new Error('Authentication failed');
        }
    };
};

module.exports = mongooseEncryption;

// Exports For Tests //
module.exports.AAC_LENGTH = AAC_LENGTH;
module.exports.VERSION_LENGTH = VERSION_LENGTH;
<commit_msg>More Mongoose 5 compatibility fixes<commit_after>'use strict';

var crypto = require('crypto');
var _ = require('underscore');
var mongoose = require('mongoose');
var semver = require('semver');
var stableStringify = require('json-stable-stringify');
var async = require('async');
var dotty = require('dotty');
var bufferEqual = require('buffer-equal-constant-time');
var mpath = require('mpath');

var objectUtil = require('../util/object-util.js');
var cryptoUtil = require('../util/crypto-util.js');

var pick = objectUtil.pick;
var setFieldValue = objectUtil.setFieldValue;
var isEmbeddedDocument = objectUtil.isEmbeddedDocument;
var drop256 = cryptoUtil.drop256;
var deriveKey = cryptoUtil.deriveKey;

/**  Plugin Constants */

var VERSION = 'a';

var ENCRYPTION_ALGORITHM = 'aes-256-cbc';
var IV_LENGTH = 16;
var AAC_LENGTH = 32;
var VERSION_LENGTH = 1;
var VERSION_BUF = new Buffer(VERSION);

/**
 * Check For Compatible Mongoose Version
 */
if(semver.gt(process.version, '4.0.0')){
    if(semver.lt(mongoose.version, '4.2.4')){
        throw new Error('Mongoose version 4.2.4 or greater is required for Node version 4.0.0 or greater');
    }
}

const isMongoose5 = semver.gte(mongoose.version, '5.0.0')

/**
 * Mongoose encryption plugin
 * @module mongoose-encryption
 *
 *
 * @param      {Object}     schema   The schema
 * @param      {Object}     options  Plugin options
 * @param      {string}     [options.secret]  A secret string which will be used to generate an encryption key and a signing key
 * @param      {string}     [options.encryptionKey]  A secret string which will be used to generate an encryption key
 * @param      {string}     [options.signingKey]  A secret string which will be used to generate a signing key
 * @param      {string[]}   [options.encryptedFields]  A list of fields to encrypt. Default is to encrypt all fields.
 * @param      {string[]}   [options.excludeFromEncryption]  A list of fields to not encrypt
 * @param      {string[]}   [options.additionalAuthenticatedFields]  A list of fields to authenticate even if they aren't encrypted
 * @param      {boolean}    [options.requireAuthenticationCode=true]  Whether documents without an authentication code are valid
 * @param      {boolean}    [options.decryptPostSave=true]  Whether to automatically decrypt documents in the application after saving them (faster if false)
 * @param      {string}     [options.collectionId]  If you update the Model name of the schema, this should be set to its original name
 * @return     {undefined}
 */

var mongooseEncryption = function(schema, options) {
    var encryptedFields, excludedFields, authenticatedFields, encryptionKey, signingKey, path;

    _.defaults(options, {
        middleware: true, // allow for skipping middleware with false
        requireAuthenticationCode: true, // allow for no authentication code on docs (not recommended),
        decryptPostSave: true, // allow for skipping the decryption after save for improved performance
        _suppressDuplicatePluginError: false // used for testing only

    });

    /** Encryption Keys */

    if (options.secret) {
        if (options.encryptionKey || options.signingKey) {
            throw new Error('if options.secret is used, then options.encryptionKey and options.signingKey must not be included');
        } else {
            encryptionKey = drop256(deriveKey(options.secret, 'enc'));
            signingKey = deriveKey(options.secret, 'sig');
        }
    } else {
        if (!options.encryptionKey || !options.signingKey) {
            throw new Error('must provide either options.secret or both options.encryptionKey and options.signingKey');
        } else {
            encryptionKey = new Buffer(options.encryptionKey, 'base64');
            if (encryptionKey.length !== 32) {
                throw new Error('options.encryptionKey must be a a 32 byte base64 string');
            }
            signingKey = new Buffer(options.signingKey, 'base64');
            if (signingKey.length !== 64) {
                throw new Error('options.signingKey must be a a 64 byte base64 string');
            }
        }
    }

    /** Deprecated options */

    if (options.fields) {
        options.encryptedFields = options.fields;
        console.warn('options.fields has been deprecated. please use options.encryptedFields');
    }
    if (options.exclude) {
        options.excludeFromEncryption = options.exclude;
        console.warn('options.fields has been deprecated. please use options.excludeFromEncryption');
    }

    /** Encryption Options */

    if (options.encryptedFields) {
        encryptedFields = _.difference(options.encryptedFields, ['_ct']);
    } else {
        excludedFields = _.union(['_id', '_ct'], options.excludeFromEncryption);
        encryptedFields = _.chain(schema.paths)
            .filter(function(pathDetails) { // exclude indexed fields
                return !pathDetails._index
            })
            .pluck('path') // get path name
            .difference(excludedFields) // exclude excluded fields
            .uniq()
            .value();
    }

    /** Authentication Options */

    var baselineAuthenticateFields = ['_id', '_ct'];

    if (options.additionalAuthenticatedFields) {
        authenticatedFields = _.union(options.additionalAuthenticatedFields, baselineAuthenticateFields);
    } else {
        authenticatedFields = baselineAuthenticateFields;
    }

    /** Augment Schema */

    if (!schema.paths._ct) { // ciphertext
        schema.add({
            _ct: {
                type: Buffer
            }
        });
    }
    if (!schema.paths._ac) { // authentication code
        schema.add({
            _ac: {
                type: Buffer
            }
        });
    }

    /** Authentication Functions */

    var computeAC = function(doc, fields, version, modelName) {
        // HMAC-SHA512-drop-256
        var hmac = crypto.createHmac('sha512', signingKey);

        if (!(fields instanceof Array)){
            throw new Error('fields must be an array');
        }
        if (fields.indexOf('_id') === -1) {
            throw new Error('_id must be in array of fields to authenticate');
        }
        if (fields.indexOf('_ac') !== -1) {
            throw new Error('_ac cannot be in array of fields to authenticate');
        }

        var collectionId = options.collectionId || modelName || doc.constructor.modelName;

        if (!collectionId) {
            throw new Error('For authentication, each collection must have a unique id. This is normally the model name when there is one, but can be overridden or added by options.collectionId');
        }

        // convert to regular object if possible in order to convert to the eventual mongo form which may be different than mongoose form
        // and only pick fields that will be authenticated
        var objectToAuthenticate = pick((doc.toObject ? doc.toObject() : doc), fields);
        var stringToAuthenticate = stableStringify(objectToAuthenticate);
        hmac.update(collectionId);
        hmac.update(version);
        hmac.update(stringToAuthenticate);
        hmac.update(JSON.stringify(fields));
        var fullAuthenticationBuffer = new Buffer(hmac.digest());
        return drop256(fullAuthenticationBuffer);
    };

    /** Functions To Check If Authenticated Fields Were Selected By Query */

    var authenticationFieldsToCheck = _.chain(authenticatedFields).union(['_ac']).without('_id').value(); // _id is implicitly selected

    var authenticatedFieldsIsSelected = function(doc){
        return _.map(authenticationFieldsToCheck, function(field) {
            return doc.isSelected(field);
        });
    };

    var allAuthenticationFieldsSelected = function(doc){
        var isSelected = authenticatedFieldsIsSelected(doc);
        if (_.uniq(isSelected).length === 1){
            return isSelected[0];
        } else {
            return false;
        }
    };

    var noAuthenticationFieldsSelected = function(doc){
        var isSelected = authenticatedFieldsIsSelected(doc);
        if (_.uniq(isSelected).length === 1){
            return isSelected[0] === false;
        } else {
            return false;
        }
    };

    /** Ensure plugin only added once per schema */
    if(schema.statics._mongooseEncryptionInstalled){
        if(!options._suppressDuplicatePluginError){
            throw new Error(
                'Mongoose encryption plugin can only be added once per schema.\n\n' +
                'If you are running migrations, please remove encryption middleware first. ' +
                'Migrations should be run in a script where `encrypt.migrations` is added to the schema, '+
                'however the standard `encrypt` middleware should not be present at the same time. '
            );
        }

    } else {
        schema.statics._mongooseEncryptionInstalled = true;
    }

    /** Middleware */

    if (options.middleware) { // defaults to true
        schema.pre('init', function(next, data) {
            if (isMongoose5) {
                // 5.x does not provide a 'next' function for this middleware since it's synchronous
                data = next
            }
            var err = null;
            try { // this hook must be synchronous for embedded docs, so everything is synchronous for code simplicity
                if (!isEmbeddedDocument(this)){ // don't authenticate embedded docs because there's no way to handle the error appropriately
                    if (allAuthenticationFieldsSelected(this)) {
                        this.authenticateSync.call(data, this.constructor.modelName);
                    } else {
                        if (!noAuthenticationFieldsSelected(this)){
                            throw new Error("Authentication failed: Only some authenticated fields were selected by the query. Either all or none of the authenticated fields (" + authenticationFieldsToCheck + ") should be selected for proper authentication.");
                        }
                    }
                }
                if (this.isSelected('_ct')){
                    this.decryptSync.call(data);
                }
            } catch (e) {
                err = e;
            }

            if (isEmbeddedDocument(this)) {
                if (err) {
                    console.error(err);
                    throw err; // note: this won't actually get thrown until save, because errors in subdoc init fns are CastErrors and aren't thrown by validate()
                }
                this._doc = data;
                if (!isMongoose5) {
                    next();
                }
                return this;
            } else {
                if (isMongoose5) {
                    if (err) throw err;
                } else {
                    return next(err);
                }
            }
        });

        schema.pre('save', function(next) {
            var that = this;
            if (this.isNew || this.isSelected('_ct') ){
                that.encrypt(function(err){
                    if (err) {
                        next(err);
                    } else {
                        if ((that.isNew || allAuthenticationFieldsSelected(that)) && !isEmbeddedDocument(that)) {
                            _.forEach(authenticatedFields, function(authenticatedField){
                                that.markModified(authenticatedField)
                            });

                            that.sign(next);
                        } else {
                            next();
                        }
                    }
                });
            } else if (allAuthenticationFieldsSelected(this) && !isEmbeddedDocument(this)) { // _ct is not selected but all authenticated fields are. cannot get hit in current version.
                _.forEach(authenticatedFields, function(authenticatedField){
                    that.markModified(authenticatedField)
                });

                this.sign(next);
            } else {
                next();
            }
        });

        if (options.decryptPostSave) { // true by default
            schema.post('save', function(doc) {
                if (isMongoose5 && isEmbeddedDocument(doc)) {
                    return doc;
                }

                if (_.isFunction(doc.decryptSync)) {
                    doc.decryptSync();
                }

                // Until 3.8.6, Mongoose didn't trigger post save hook on EmbeddedDocuments,
                // instead had to call decrypt on all subDocs.
                // ref https://github.com/LearnBoost/mongoose/issues/915

                doc._decryptEmbeddedDocs();

                return doc;
            });
        }
    }

    /** Encryption Instance Methods */

    schema.methods.encrypt = function(cb) {
        var that = this;

        if(this._ct){
            return cb(new Error('Encrypt failed: document already contains ciphertext'));
        }

        // generate random iv
        crypto.randomBytes(IV_LENGTH, function(err, iv) {
            var cipher, jsonToEncrypt, objectToEncrypt;
            if (err) {
                return cb(err);
            }
            cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, encryptionKey, iv);
            objectToEncrypt = pick(that, encryptedFields, {excludeUndefinedValues: true});

            jsonToEncrypt = JSON.stringify(objectToEncrypt);

            cipher.end(jsonToEncrypt, 'utf-8', function() {
                // add ciphertext to document
                that._ct = Buffer.concat([VERSION_BUF, iv, cipher.read()]);

                // remove encrypted fields from cleartext
                encryptedFields.forEach(function(field){
                    setFieldValue(that, field, undefined);
                });

                cb(null);
            });
        });
    };

    schema.methods.decrypt = function(cb) { // callback style but actually synchronous to allow for decryptSync without copypasta or complication
        try {
            schema.methods.decryptSync.call(this);
        } catch(e){
            return cb(e);
        }
        cb();
    };

    schema.methods.decryptSync = function() {
        var that = this;
        var ct, ctWithIV, decipher, iv, idString, decryptedObject, decryptedObjectJSON, decipheredVal;
        if (this._ct) {
            ctWithIV = this._ct.hasOwnProperty('buffer') ? this._ct.buffer : this._ct;
            iv = ctWithIV.slice(VERSION_LENGTH, VERSION_LENGTH + IV_LENGTH);
            ct = ctWithIV.slice(VERSION_LENGTH + IV_LENGTH, ctWithIV.length);

            decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, encryptionKey, iv);
            try {
                decryptedObjectJSON = decipher.update(ct, undefined, 'utf8') + decipher.final('utf8');
                decryptedObject = JSON.parse(decryptedObjectJSON);
            } catch (err) {
                if (this._id) {
                    idString = this._id.toString();
                } else {
                    idString = 'unknown';
                }
                throw new Error('Error parsing JSON during decrypt of ' + idString + ': ' + err);
            }

            encryptedFields.forEach(function(field) {
                decipheredVal = mpath.get(field, decryptedObject);

                //JSON.parse returns {type: "Buffer", data: Buffer} for Buffers
                //https://nodejs.org/api/buffer.html#buffer_buf_tojson
                if(_.isObject(decipheredVal) && decipheredVal.type === "Buffer"){
                    setFieldValue(that, field, decipheredVal.data);
                }else {
                    setFieldValue(that, field, decipheredVal);
                }
            });

            this._ct = undefined;
            this._ac = undefined;
        }
    };

    /**
     * Decrypt any embedded documents inside of this document
     *
     * @private
     * Should not be needed outside plugin
     */
    schema.methods._decryptEmbeddedDocs = function () {
        var doc = this;
        _.keys(this.schema.paths).forEach(function (path) {
            if (path === '_id' || path === '__v') {
                return;
            }

            var nestedDoc = dotty.get(doc, path);

            if (nestedDoc && nestedDoc[0] && isEmbeddedDocument(nestedDoc[0])) {
                nestedDoc.forEach(function (subDoc) {
                    if (_.isFunction(subDoc.decryptSync)) {
                        subDoc.decryptSync();
                    }
                });
            }
        });
    };

    /** Authentication Instance Methods */

    schema.methods.sign = function(cb) {
        var basicAC = computeAC(this, authenticatedFields, VERSION);
        var authenticatedFieldsBuf = new Buffer(JSON.stringify(authenticatedFields));
        this._ac = Buffer.concat([VERSION_BUF, basicAC, authenticatedFieldsBuf]);
        cb();
    };

    schema.methods.authenticate = function(cb) { // callback style but actually synchronous to allow for decryptSync without copypasta or complication
        try {
            schema.methods.authenticateSync.call(this);
        } catch(e){
            return cb(e);
        }
        cb();
    };

    schema.methods.authenticateSync = function() {
        if (!this._ac) {
            if (options.requireAuthenticationCode) {
                throw new Error('Authentication code missing');
            } else {
                return null;
            }
        }
        var acBuf = this._ac.hasOwnProperty('buffer') ? this._ac.buffer : this._ac;
        if (acBuf.length < VERSION_LENGTH + AAC_LENGTH + 2) {
            throw new Error('_ac is too short and has likely been cut off or modified');
        }
        var versionUsed = acBuf.slice(0, VERSION_LENGTH).toString();
        var basicAC = acBuf.slice(VERSION_LENGTH, VERSION_LENGTH + AAC_LENGTH);
        var authenticatedFieldsUsed = JSON.parse(acBuf.slice(VERSION_LENGTH + AAC_LENGTH, acBuf.length).toString());

        var expectedHMAC = computeAC(this, authenticatedFieldsUsed, versionUsed, arguments[0]); // pass in modelName as argument in init hook

        var authentic = bufferEqual(basicAC, expectedHMAC);
        if (!authentic){
            throw new Error('Authentication failed');
        }
    };
};

module.exports = mongooseEncryption;

// Exports For Tests //
module.exports.AAC_LENGTH = AAC_LENGTH;
module.exports.VERSION_LENGTH = VERSION_LENGTH;
<|endoftext|>


<commit_before>angular
.module('app', [], function($routeProvider, $locationProvider) {
  $locationProvider.html5Mode(true);

  $routeProvider
    .when('/', {
      templateUrl: '/partials/create.html',
      controller: CreateCtrl
    })
    .when('/q/:qid', {
      templateUrl: '/partials/q.html',
      controller: QCtrl
    })
    ;
})
.factory('ws', function() {
  var ws = new WebSocket('ws://' + location.host);
  ws.cb = {};
  ws.onopen = function() {
    ws._emit('connect');
  };
  ws.onerror = function(error) {
    ws._emit('error', error);
  };
  ws.onclose = function() {
    ws._emit('disconnect');
  };
  ws.onmessage = function(msg) {
    var data = JSON.parse(msg.data);
    ws._emit(data.name, data.args);
  };
  ws.on = function(event, cb) {
    ws.cb[event] || (ws.cb[event] = []);
    ws.cb[event].push(cb);
  };
  ws._emit = function(event, arg) {
    angular.forEach(ws.cb[event], function(cb) {
      cb(arg);
    });
  };
  ws.emit = function() {
    var args = Array.prototype.slice.call(arguments);
    ws.send(JSON.stringify(args));
  };
  return ws;
})
.directive('card', function() {
  return {
    restrict: 'E',
    template: ''
  };
})
;

function CreateCtrl($scope, $http, $location) {
  $scope.size = 8;
  $scope.sets = [
    'Shards of Alara',
    'Conflux',
    'Alara Reborn',
    'Zendikar',
    'Worldwake',
    'Rise of the Eldrazi',
    'Magic 2011',
    'Scars of Mirrodin',
    'Mirrodin Besieged',
    'New Phyrexia',
    'Magic 2012',
    'Innistrad',
    'Dark Ascension',
    'Avacyn Restored',
    'Magic 2013',
    'Return to Ravnica',
    'Gatecrash'
  ];
  $scope.set1 = 'Gatecrash';
  $scope.set2 = 'Gatecrash';
  $scope.set3 = 'Gatecrash';
  $scope.set4 = 'Gatecrash';
  $scope.set5 = 'Gatecrash';
  $scope.set6 = 'Gatecrash';
  $scope.create = function() {
    var sets = [$scope.set1, $scope.set2, $scope.set3, $scope.set4, $scope.set5, $scope.set6];
    $http.post('/create', { sets: sets, sealed: $scope.sealed, size: $scope.size })
      .success(function(data, status) {
        $location.path('/q/' + data.id);
      })
      ;
  };
}

function QCtrl($scope, $timeout, $http, $routeParams, ws) {
  $scope.main = [];
  $scope.side = [];
  $scope.land = [
  { land: true, id: 73946, name: 'Forest'   },
  { land: true, id: 73951, name: 'Island'   },
  { land: true, id: 73958, name: 'Mountain' },
  { land: true, id: 73963, name: 'Plains'   },
  { land: true, id: 73973, name: 'Swamp'    }
  ];

  localStorage.pid || (localStorage.pid = Math.floor(Math.random() * 1e8));

  function decrement() {
    angular.forEach($scope.players, function(player) {
      if (player.time)
        --player.time;
    });
    $timeout(decrement, 1000);
  }

  $timeout(decrement, 1000);

  ws.on('connect', function() {
    var qid = $routeParams.qid
    , pid = localStorage.pid
    , name = localStorage.name
    ;
    ws.emit('init', qid, pid, name);
  });
  ws.on('error', function(error) {
    $scope.error = error;
    $scope.$apply();
  });

  ws.on('meta', function(meta) {
    var players = meta.players
      , index = meta.index
      , ended = meta.ended
      , size = meta.size
      , oppIndex
      ;
    if (size === 8)// XXX magic
      oppIndex = (index + (size/2)) % size;
    $scope.end = ended;
    $scope.players = players;
    $scope.self = $scope.players[index];
    $scope.self.self = true;
    var opp = players[oppIndex];
    if (opp)
      opp.opponent = true;
    if (!$scope.self.name)
      $scope.self.edit = true;
    $scope.$apply();
  });
  ws.on('pack', function(pack) {
    if (pack) {
      pack.show = true;
      if (pack.cards.length && $scope.beep)
        document.querySelector('audio').play();
      angular.forEach(pack.cards, function(card) {
        if (!card.imgURL)
          card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
      });
    }
    $scope.pack = pack;
    $scope.$apply();
  });
  ws.on('pick', function(card) {
    if (!card.imgURL)
      card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
    $scope.main.push(card);
    $scope.$apply();
  });
  ws.on('picks', function(cards) {
    angular.forEach(cards, function(card) {
      if (!card.imgURL)
        card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
    });
    $scope.main = cards;
    $scope.$apply();
  });
  ws.on('disconnect', function() {
    $scope.end = true;
    $scope.$apply();
  });

  $scope.pick = function(card) {
    ws.emit('pick', $scope.pack.id, card.id);
    $scope.pack.show = false;
  };
  $scope.name = function(name) {
    $scope.self.edit = false;
    ws.emit('name', name);
    localStorage.name = name;
  };
  $scope.toSide = function(card) {
    var main = $scope.main;
    main.splice(main.indexOf(card), 1);
    if (!card.land)
      $scope.side.push(card);
  };
  $scope.toMain = function(card) {
    var side = $scope.side;
    if (!card.land)
      side.splice(side.indexOf(card), 1);
    $scope.main.push(card);
  };
  $scope.generateDeck = function() {
    var main = {}
      , side = {}
      ;
    angular.forEach($scope.main, function(card) {
      var name = card.name;
      main[name] || (main[name] = 0);
      main[name] += 1;
    });
    angular.forEach($scope.side, function(card) {
      var name = card.name;
      side[name] || (side[name] = 0);
      side[name] += 1;
    });
    $scope.deck = {
      main: main,
      side: side
    };
    $scope.deckJSON = JSON.stringify($scope.deck);
  };
  $scope.hash = function() {
    $scope.generateDeck();
    ws.emit('hash', $scope.deck);
  };
}
<commit_msg>show size<commit_after>angular
.module('app', [], function($routeProvider, $locationProvider) {
  $locationProvider.html5Mode(true);

  $routeProvider
    .when('/', {
      templateUrl: '/partials/create.html',
      controller: CreateCtrl
    })
    .when('/q/:qid', {
      templateUrl: '/partials/q.html',
      controller: QCtrl
    })
    ;
})
.factory('ws', function() {
  var ws = new WebSocket('ws://' + location.host);
  ws.cb = {};
  ws.onopen = function() {
    ws._emit('connect');
  };
  ws.onerror = function(error) {
    ws._emit('error', error);
  };
  ws.onclose = function() {
    ws._emit('disconnect');
  };
  ws.onmessage = function(msg) {
    var data = JSON.parse(msg.data);
    ws._emit(data.name, data.args);
  };
  ws.on = function(event, cb) {
    ws.cb[event] || (ws.cb[event] = []);
    ws.cb[event].push(cb);
  };
  ws._emit = function(event, arg) {
    angular.forEach(ws.cb[event], function(cb) {
      cb(arg);
    });
  };
  ws.emit = function() {
    var args = Array.prototype.slice.call(arguments);
    ws.send(JSON.stringify(args));
  };
  return ws;
})
.directive('card', function() {
  return {
    restrict: 'E',
    template: ''
  };
})
;

function CreateCtrl($scope, $http, $location) {
  $scope.size = 8;
  $scope.sets = [
    'Shards of Alara',
    'Conflux',
    'Alara Reborn',
    'Zendikar',
    'Worldwake',
    'Rise of the Eldrazi',
    'Magic 2011',
    'Scars of Mirrodin',
    'Mirrodin Besieged',
    'New Phyrexia',
    'Magic 2012',
    'Innistrad',
    'Dark Ascension',
    'Avacyn Restored',
    'Magic 2013',
    'Return to Ravnica',
    'Gatecrash'
  ];
  $scope.set1 = 'Gatecrash';
  $scope.set2 = 'Gatecrash';
  $scope.set3 = 'Gatecrash';
  $scope.set4 = 'Gatecrash';
  $scope.set5 = 'Gatecrash';
  $scope.set6 = 'Gatecrash';
  $scope.create = function() {
    var sets = [$scope.set1, $scope.set2, $scope.set3, $scope.set4, $scope.set5, $scope.set6];
    $http.post('/create', { sets: sets, sealed: $scope.sealed, size: $scope.size })
      .success(function(data, status) {
        $location.path('/q/' + data.id);
      })
      ;
  };
}

function QCtrl($scope, $timeout, $http, $routeParams, ws) {
  $scope.main = [];
  $scope.side = [];
  $scope.land = [
  { land: true, id: 73946, name: 'Forest'   },
  { land: true, id: 73951, name: 'Island'   },
  { land: true, id: 73958, name: 'Mountain' },
  { land: true, id: 73963, name: 'Plains'   },
  { land: true, id: 73973, name: 'Swamp'    }
  ];

  localStorage.pid || (localStorage.pid = Math.floor(Math.random() * 1e8));

  function decrement() {
    angular.forEach($scope.players, function(player) {
      if (player.time)
        --player.time;
    });
    $timeout(decrement, 1000);
  }

  $timeout(decrement, 1000);

  ws.on('connect', function() {
    var qid = $routeParams.qid
    , pid = localStorage.pid
    , name = localStorage.name
    ;
    ws.emit('init', qid, pid, name);
  });
  ws.on('error', function(error) {
    $scope.error = error;
    $scope.$apply();
  });

  ws.on('meta', function(meta) {
    var players = meta.players
      , index = meta.index
      , ended = meta.ended
      , size = meta.size
      , oppIndex
      ;
    if (size === 8)// XXX magic
      oppIndex = (index + (size/2)) % size;
    while(size > players.length)
      players.push({});
    $scope.end = ended;
    $scope.players = players;
    $scope.self = $scope.players[index];
    $scope.self.self = true;
    var opp = players[oppIndex];
    if (opp)
      opp.opponent = true;
    if (!$scope.self.name)
      $scope.self.edit = true;
    $scope.$apply();
  });
  ws.on('pack', function(pack) {
    if (pack) {
      pack.show = true;
      if (pack.cards.length && $scope.beep)
        document.querySelector('audio').play();
      angular.forEach(pack.cards, function(card) {
        if (!card.imgURL)
          card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
      });
    }
    $scope.pack = pack;
    $scope.$apply();
  });
  ws.on('pick', function(card) {
    if (!card.imgURL)
      card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
    $scope.main.push(card);
    $scope.$apply();
  });
  ws.on('picks', function(cards) {
    angular.forEach(cards, function(card) {
      if (!card.imgURL)
        card.imgURL = "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=" + card.id + "&type=card";
    });
    $scope.main = cards;
    $scope.$apply();
  });
  ws.on('disconnect', function() {
    $scope.end = true;
    $scope.$apply();
  });

  $scope.pick = function(card) {
    ws.emit('pick', $scope.pack.id, card.id);
    $scope.pack.show = false;
  };
  $scope.name = function(name) {
    $scope.self.edit = false;
    ws.emit('name', name);
    localStorage.name = name;
  };
  $scope.toSide = function(card) {
    var main = $scope.main;
    main.splice(main.indexOf(card), 1);
    if (!card.land)
      $scope.side.push(card);
  };
  $scope.toMain = function(card) {
    var side = $scope.side;
    if (!card.land)
      side.splice(side.indexOf(card), 1);
    $scope.main.push(card);
  };
  $scope.generateDeck = function() {
    var main = {}
      , side = {}
      ;
    angular.forEach($scope.main, function(card) {
      var name = card.name;
      main[name] || (main[name] = 0);
      main[name] += 1;
    });
    angular.forEach($scope.side, function(card) {
      var name = card.name;
      side[name] || (side[name] = 0);
      side[name] += 1;
    });
    $scope.deck = {
      main: main,
      side: side
    };
    $scope.deckJSON = JSON.stringify($scope.deck);
  };
  $scope.hash = function() {
    $scope.generateDeck();
    ws.emit('hash', $scope.deck);
  };
}
<|endoftext|>


<commit_before>var gulp = require('gulp');

gulp.task('lint', function () {

  var jshint = require('gulp-jshint');
  var stylish = require('jshint-stylish');
  var jscs = require('gulp-jscs');

  var config = require('../config').js;

  gulp.src(config.src)
    .pipe(jscs())
    .pipe(jshint())
    .pipe(jshint.reporter(stylish));
});
<commit_msg>refactor how js config is required<commit_after>var gulp = require('gulp');

gulp.task('lint', function () {

  var jshint = require('gulp-jshint');
  var stylish = require('jshint-stylish');
  var jscs = require('gulp-jscs');

  var config = require('../config');

  gulp.src(config.js.src)
    .pipe(jscs())
    .pipe(jshint())
    .pipe(jshint.reporter(stylish));
});
<|endoftext|>


<commit_before>/* eslint-disable
    handle-callback-err,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS201: Simplify complex destructure assignments
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
let ResourceStateManager
const Path = require('path')
const fs = require('fs')
const logger = require('logger-sharelatex')
const settings = require('settings-sharelatex')
const Errors = require('./Errors')
const SafeReader = require('./SafeReader')

module.exports = ResourceStateManager = {
  // The sync state is an identifier which must match for an
  // incremental update to be allowed.
  //
  // The initial value is passed in and stored on a full
  // compile, along with the list of resources..
  //
  // Subsequent incremental compiles must come with the same value - if
  // not they will be rejected with a 409 Conflict response. The
  // previous list of resources is returned.
  //
  // An incremental compile can only update existing files with new
  // content.  The sync state identifier must change if any docs or
  // files are moved, added, deleted or renamed.

  SYNC_STATE_FILE: '.project-sync-state',
  SYNC_STATE_MAX_SIZE: 128 * 1024,

  saveProjectState(state, resources, basePath, callback) {
    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
    if (state == null) {
      // remove the file if no state passed in
      logger.log({ state, basePath }, 'clearing sync state')
      fs.unlink(stateFile, function (err) {
        if (err && err.code !== 'ENOENT') {
          return callback(err)
        } else {
          return callback()
        }
      })
    } else {
      logger.log({ state, basePath }, 'writing sync state')
      const resourceList = resources.map((resource) => resource.path)
      fs.writeFile(
        stateFile,
        [...resourceList, `stateHash:${state}`].join('\n'),
        callback
      )
    }
  },

  checkProjectStateMatches(state, basePath, callback) {
    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
    const size = this.SYNC_STATE_MAX_SIZE
    SafeReader.readFile(stateFile, size, 'utf8', function (
      err,
      result,
      bytesRead
    ) {
      if (err) {
        return callback(err)
      }
      if (bytesRead === size) {
        logger.error(
          { file: stateFile, size, bytesRead },
          'project state file truncated'
        )
      }
      const array = result.toString().split('\n')
      const adjustedLength = Math.max(array.length, 1)
      const resourceList = array.slice(0, adjustedLength - 1)
      const oldState = array[adjustedLength - 1]
      const newState = `stateHash:${state}`
      logger.log(
        { state, oldState, basePath, stateMatches: newState === oldState },
        'checking sync state'
      )
      if (newState !== oldState) {
        return callback(
          new Errors.FilesOutOfSyncError('invalid state for incremental update')
        )
      } else {
        const resources = resourceList.map((path) => ({ path }))
        callback(null, resources)
      }
    })
  },

  checkResourceFiles(resources, allFiles, basePath, callback) {
    // check the paths are all relative to current directory
    let file
    const containsRelativePath = (resource) => {
      const dirs = resource.path.split('/')
      return dirs.indexOf('..') !== -1
    }
    if (resources.some(containsRelativePath)) {
      return callback(new Error('relative path in resource file list'))
    }
    // check if any of the input files are not present in list of files
    const seenFiles = new Set(allFiles)
    const missingFiles = resources
      .map((resource) => resource.path)
      .filter((path) => !seenFiles.has(path))
    if (missingFiles.length > 0) {
      logger.err(
        { missingFiles, basePath, allFiles, resources },
        'missing input files for project'
      )
      return callback(
        new Errors.FilesOutOfSyncError(
          'resource files missing in incremental update'
        )
      )
    } else {
      callback()
    }
  },
}
<commit_msg>remove unused require<commit_after>/* eslint-disable
    handle-callback-err,
    no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
 * decaffeinate suggestions:
 * DS201: Simplify complex destructure assignments
 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
 */
let ResourceStateManager
const Path = require('path')
const fs = require('fs')
const logger = require('logger-sharelatex')
const Errors = require('./Errors')
const SafeReader = require('./SafeReader')

module.exports = ResourceStateManager = {
  // The sync state is an identifier which must match for an
  // incremental update to be allowed.
  //
  // The initial value is passed in and stored on a full
  // compile, along with the list of resources..
  //
  // Subsequent incremental compiles must come with the same value - if
  // not they will be rejected with a 409 Conflict response. The
  // previous list of resources is returned.
  //
  // An incremental compile can only update existing files with new
  // content.  The sync state identifier must change if any docs or
  // files are moved, added, deleted or renamed.

  SYNC_STATE_FILE: '.project-sync-state',
  SYNC_STATE_MAX_SIZE: 128 * 1024,

  saveProjectState(state, resources, basePath, callback) {
    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
    if (state == null) {
      // remove the file if no state passed in
      logger.log({ state, basePath }, 'clearing sync state')
      fs.unlink(stateFile, function (err) {
        if (err && err.code !== 'ENOENT') {
          return callback(err)
        } else {
          return callback()
        }
      })
    } else {
      logger.log({ state, basePath }, 'writing sync state')
      const resourceList = resources.map((resource) => resource.path)
      fs.writeFile(
        stateFile,
        [...resourceList, `stateHash:${state}`].join('\n'),
        callback
      )
    }
  },

  checkProjectStateMatches(state, basePath, callback) {
    const stateFile = Path.join(basePath, this.SYNC_STATE_FILE)
    const size = this.SYNC_STATE_MAX_SIZE
    SafeReader.readFile(stateFile, size, 'utf8', function (
      err,
      result,
      bytesRead
    ) {
      if (err) {
        return callback(err)
      }
      if (bytesRead === size) {
        logger.error(
          { file: stateFile, size, bytesRead },
          'project state file truncated'
        )
      }
      const array = result.toString().split('\n')
      const adjustedLength = Math.max(array.length, 1)
      const resourceList = array.slice(0, adjustedLength - 1)
      const oldState = array[adjustedLength - 1]
      const newState = `stateHash:${state}`
      logger.log(
        { state, oldState, basePath, stateMatches: newState === oldState },
        'checking sync state'
      )
      if (newState !== oldState) {
        return callback(
          new Errors.FilesOutOfSyncError('invalid state for incremental update')
        )
      } else {
        const resources = resourceList.map((path) => ({ path }))
        callback(null, resources)
      }
    })
  },

  checkResourceFiles(resources, allFiles, basePath, callback) {
    // check the paths are all relative to current directory
    let file
    const containsRelativePath = (resource) => {
      const dirs = resource.path.split('/')
      return dirs.indexOf('..') !== -1
    }
    if (resources.some(containsRelativePath)) {
      return callback(new Error('relative path in resource file list'))
    }
    // check if any of the input files are not present in list of files
    const seenFiles = new Set(allFiles)
    const missingFiles = resources
      .map((resource) => resource.path)
      .filter((path) => !seenFiles.has(path))
    if (missingFiles.length > 0) {
      logger.err(
        { missingFiles, basePath, allFiles, resources },
        'missing input files for project'
      )
      return callback(
        new Errors.FilesOutOfSyncError(
          'resource files missing in incremental update'
        )
      )
    } else {
      callback()
    }
  },
}
<|endoftext|>


<commit_before>define(['js/utils','text!apps/enriches/round_template.html'], function(u,round) {
	console.log('foo');

	var assert = u.assert, deferred = u.deferred, defined = u.defined;

	var example_round = {
		text: "M & S Kings X",
		name: { begin: 0, end: 4 },
		location: { begin: 5, end: 15 },
		categories: ['groceries']
	};
	var MatchesView = Backbone.View.extend({
		update:function(results) {
			var this_ = this;
			var button_templ = "<div class='btn' name='<%= text %>'><%= text %>
";
			this.$el.html(''); // children().remove();
			results.map(function(b) {
				console.log(b);
				this_.$el.append(_(button_templ).template({text:b}));
			});
		},
		render:function() { return this; }
	});
	var RoundView = Backbone.View.extend({
		template:round,
		tagClass:'round',
		events: {
			'select .select-name' : '_cb_name_input_selection',
			'select .select-location' : '_cb_location_input_sel'
		},
		initialize:function(options) {
			assert(options.round, 'please provide a round as an argument');
		},
		_cb_location_input_sel:function(evt) {
			var start = evt.target.selectionStart, end = evt.target.selectionEnd;
			var val = $(evt.target).val().substring(start,end);
			this.$el.find('.display-selected-location').val(val);
			this.$el.find('.input-location').focus();
			this.loc_matches_view.update([val]);
		},
		_cb_name_input_selection:function(evt) {
			var start = evt.target.selectionStart, end = evt.target.selectionEnd;
			var val = $(evt.target).val().substring(start,end);
			this.$el.find('.display-selected-name').val(val);
			this.$el.find('.input-name').focus();
			this.name_matches_view.update([val]);
		},
		render:function() {
			var html = _(this.template).template(this.options.round);
			this.$el.html( html );
			var cats = this.$el.find('.categories-input');
			cats.children('.option').remove();
			$.ajax({url:'categories/cat-simple.json', type:"GET", dataType:"json"}).success(function(result) {
				result.categories.map(function(c) {
					var h = _('<option value="<%= text %>"><%= text %></option>').template({text:c});
					cats.append(h);
				});
				$(".chzn-select").chosen({no_results_text: "No results matched"});
			}).error(function(f) {	console.log("FAIL ", f);});

			this.$el.find('.input-location').typeahead({ source: function(q,process) {
				// put an ajax call to thingy now
				var locs = ['london', 'dublin', 'berlin', 'southampton'];
				process(locs.filter(function(f) { return f.indexOf(q) == 0; }));
			}});
			this.$el.find('.input-name').typeahead({ source: function(q,process) {
				var locs = ['marks & spencers', 'john lewis', 'harrods'];
				process(locs.filter(function(f) { return f.indexOf(q) == 0; }));
			}});

			this.loc_matches_view = new MatchesView({el:this.$el.find('.match-location')});
			this.name_matches_view = new MatchesView({el:this.$el.find('.match-name')});
			return this;
		}
	});
	var EnrichView = Backbone.View.extend({
		show_round:function(round) {
			console.log('showing round >> ', round);
			var roundview = new RoundView({round:round});
			this.roundview = roundview;
			this.$el.find('.round-holder').children().remove();
			this.$el.find('.round-holder').append(roundview.render().el);

			this.render();
		},
		render:function() {
			return this;
		}
	});

	var EnrichApp = Backbone.Model.extend({
		initialize:function() {
			this.view = new EnrichView({el:$('.main')[0]});
			$('body').append(this.view.render().el);
			this.view.show_round(example_round);
		},
		show:function() {
			this.view.$el.show();
		},
		hide:function() {
			this.view.$el.hide();
		}
	});

	return {
		init:function() {
			return new EnrichApp();
		}
	};
});
<commit_msg>now provides an autocomplete dude<commit_after>define(['js/utils','text!apps/enriches/round_template.html'], function(u,round) {
	console.log('foo');

	var assert = u.assert, deferred = u.deferred, defined = u.defined;

	var example_round = {
		text: "M & S Kings X",
		name: { begin: 0, end: 4 },
		location: { begin: 5, end: 15 },
		categories: ['groceries']
	};
	var MatchesView = Backbone.View.extend({
		events: {
			'click .btn' : 'click'
		},
		update:function(results) {
			var this_ = this;
			var button_templ = "<div class='btn' name='<%= text %>'><%= text %>
";
			this.$el.html(''); // children().remove();
			results.map(function(b) {
				console.log(b);
				this_.$el.append(_(button_templ).template({text:b}));
			});
		},
		click:function(evt) {
			this.trigger('click', $(evt.currentTarget).attr('name'));
		},
		render:function() { return this; }
	});
	var RoundView = Backbone.View.extend({
		template:round,
		tagClass:'round',
		events: {
			'select .select-name' : '_cb_name_input_selection',
			'select .select-location' : '_cb_location_input_sel'
		},
		initialize:function(options) {
			assert(options.round, 'please provide a round as an argument');
		},
		_cb_location_input_sel:function(evt) {
			var start = evt.target.selectionStart, end = evt.target.selectionEnd;
			var val = $(evt.target).val().substring(start,end);
			this.$el.find('.display-selected-location').val(val);
			this.$el.find('.input-location').focus();
			this.loc_matches_view.update([val]);
		},
		_cb_name_input_selection:function(evt) {
			var start = evt.target.selectionStart, end = evt.target.selectionEnd;
			var val = $(evt.target).val().substring(start,end);
			this.$el.find('.display-selected-name').val(val);
			this.$el.find('.input-name').focus();
			this.name_matches_view.update([val]);
		},
		render:function() {
			var this_ = this;
			var html = _(this.template).template(this.options.round);
			this.$el.html( html );
			var cats = this.$el.find('.categories-input');
			cats.children('.option').remove();
			$.ajax({url:'categories/cat-simple.json', type:"GET", dataType:"json"}).success(function(result) {
				result.categories.map(function(c) {
					var h = _('<option value="<%= text %>"><%= text %></option>').template({text:c});
					cats.append(h);
				});
				$(".chzn-select").chosen({no_results_text: "No results matched"});
			}).error(function(f) {	console.log("FAIL ", f);});

			this.$el.find('.input-location').typeahead({ source: function(q,process) {
				// put an ajax call to thingy now
				var locs = ['london', 'dublin', 'berlin', 'southampton'];
				process(locs.filter(function(f) { return f.indexOf(q) == 0; }));
			}});
			this.$el.find('.input-name').typeahead({ source: function(q,process) {
				var locs = ['marks & spencers', 'john lewis', 'harrods'];
				process(locs.filter(function(f) { return f.indexOf(q) == 0; }));
			}});

			this.loc_matches_view = new MatchesView({el:this.$el.find('.match-location')});
			this.loc_matches_view.on('click', function(what) {
				this_.$el.find('.input-location').val(what);
			});
			this.name_matches_view = new MatchesView({el:this.$el.find('.match-name')});
			this.name_matches_view.on('click', function(what) {
				this_.$el.find('.input-name').val(what);
			});
			return this;
		}
	});
	var EnrichView = Backbone.View.extend({
		show_round:function(round) {
			console.log('showing round >> ', round);
			var roundview = new RoundView({round:round});
			this.roundview = roundview;
			this.$el.find('.round-holder').children().remove();
			this.$el.find('.round-holder').append(roundview.render().el);

			this.render();
		},
		render:function() {
			return this;
		}
	});

	var EnrichApp = Backbone.Model.extend({
		initialize:function() {
			this.view = new EnrichView({el:$('.main')[0]});
			$('body').append(this.view.render().el);
			this.view.show_round(example_round);
		},
		show:function() {
			this.view.$el.show();
		},
		hide:function() {
			this.view.$el.hide();
		}
	});

	return {
		init:function() {
			return new EnrichApp();
		}
	};
});
<|endoftext|>


<commit_before>// @flow
import htmlparser from 'htmlparser2'
import cssSelect from 'css-select'
export {cssSelect}

export function parseHtml(string: string): Object {
  return htmlparser.parseDOM(string, {
    withDomLvl1: true,
    normalizeWhitespace: false,
    xmlMode: false,
    decodeEntities: true,
  })
}

// from https://github.com/fb55/domutils/blob/master/lib/stringify.js
export function getText(elem: Object | Object[]): string {
  if (Array.isArray(elem)) return elem.map(getText).join('')
  if (elem.type === 'tag') return getText(elem.children)
  if (elem.type === 'text') return elem.data
  return ''
}

function getTextWithSpaces(elem: Object | Object[]): string {
  if (Array.isArray(elem)) return elem.map(getTextWithSpaces).join(' ')
  if (elem.type === 'tag') return getTextWithSpaces(elem.children)
  if (elem.type === 'text') return elem.data
  return ''
}

export function getTrimmedTextWithSpaces(elem: Object | Object[]): string {
  return getTextWithSpaces(elem).split(/\s+/).join(' ').trim()
}

export function removeHtmlWithRegex(str: string): string {
  return str.replace(/<[^>]*>|\s+/g, ' ')
}

export function fastGetTrimmedText(str: string): string {
  return removeHtmlWithRegex(str).replace(/\s+/g, ' ').trim()
}
<commit_msg>remove \s+ from the html removal<commit_after>// @flow
import htmlparser from 'htmlparser2'
import cssSelect from 'css-select'
export {cssSelect}

export function parseHtml(string: string): Object {
  return htmlparser.parseDOM(string, {
    withDomLvl1: true,
    normalizeWhitespace: false,
    xmlMode: false,
    decodeEntities: true,
  })
}

// from https://github.com/fb55/domutils/blob/master/lib/stringify.js
export function getText(elem: Object | Object[]): string {
  if (Array.isArray(elem)) return elem.map(getText).join('')
  if (elem.type === 'tag') return getText(elem.children)
  if (elem.type === 'text') return elem.data
  return ''
}

function getTextWithSpaces(elem: Object | Object[]): string {
  if (Array.isArray(elem)) return elem.map(getTextWithSpaces).join(' ')
  if (elem.type === 'tag') return getTextWithSpaces(elem.children)
  if (elem.type === 'text') return elem.data
  return ''
}

export function getTrimmedTextWithSpaces(elem: Object | Object[]): string {
  return getTextWithSpaces(elem).split(/\s+/).join(' ').trim()
}

export function removeHtmlWithRegex(str: string): string {
  return str.replace(/<[^>]*>/g, ' ')
}

export function fastGetTrimmedText(str: string): string {
  return removeHtmlWithRegex(str).replace(/\s+/g, ' ').trim()
}
<|endoftext|>


<commit_before>// View on GitHub: https://github.com/xPaw/CF-ABetterBrowser

CloudFlare.define( 'abetterbrowser', [ 'cloudflare/dom', 'cloudflare/user', 'abetterbrowser/config' ], function( version, user, config )
{
	"use strict";

	version = version.internetExplorer;
	var maxVersion = parseInt( config.ie, 10 ) || 8;

	/**
	 * Is user using IE? Is it old? Did user close this message already?
	 */
	if( version === undefined || version > maxVersion || user.getCookie( 'cfapp_abetterbrowser' ) === 1 )
	{
		return true;
	}

	/**
	 * Let's collect some referral statistics with link shortener service
	 */
	var moreInformationLink = 'http://goo.gl/OVoHF',

	doc = document,

	/**
	 * Detect user's browser language
	 *
	 * This is aimed at IE, so we don't try to get navigator.language
	 */
	language = window.navigator.browserLanguage || 'en',

	/**
	 * Translations
	 */
	translations =
	{
		'en': 'You are using an outdated browser. More information »',
		'is': 'Þú ert að nota úreltan vafra. Nánari upplýsingar »',
		'ru': 'Вы используете устаревший браузер. Подробнее »',
		'fr': 'Votre navigateur n\'est pas à jour. Plus d\'information »',
		'da': 'Du bruger en ældre browser. Mere information »',
		'hu': 'A böngészője elavult. További információ »',
		'de': 'Sie benutzen einen veralteten Browser. Mehr Informationen »',
		'ja': 'あなたは、時代遅れのブラウザを使用しています。詳細情報 »',
		'fa': 'شما از مرورگری قدیمی استفاده می کنید. »اطلاعات بیشتر',
		'tr': 'Çok eski bir tarayıcı kullanıyorsunuz. Daha fazla bilgi »',
		'pt': 'Você está usando um navegador antigo. Mais informações »',
		'pl': 'Używasz przestarzałej przeglądarki. Więcej informacji »',
		'fi': 'Käytät vanhentunutta selainta. Lisää tietoa »',
		'zh': '您的浏览器版本过旧。 更多信息 »',
		'es': 'Su navegador está obsoleto. Más información »',
		'nl': 'U gebruikt op dit moment een verouderde webbrowser. Meer informatie »',
		'vi': 'Trình duyệt bạn dùng đã lỗi thời rồi. Thêm thông tin »',
		'he': 'דפדפן האינטרנט שלך אינו מעודכן. למידע נוסף »',
		'it': 'Stai usando un browser datato. Ulteriori informazioni »',
		'el': 'Χρησιμοποιείτε ένα ξεπερασμένο πρόγραμμα περιήγησης. Περισσότερες πληροφορίες »',
		'id': 'Anda menggunakan web browser versi lama. Informasi selengkapnya »',
		'sr': 'Vi koristite zastarelu verziju browsera. Više informacija »',
		'zh-tw': '你正在使用過時的瀏覽器。 更多訊息 »'
	},

	/**
	 * Style rules
	 */
	rules =
		'#cloudflare-old-browser {' +
			'background:#45484d;' +
			'position:absolute;' +
			'z-index:100000;' +
			'width:100%;' +
			'height:26px;' +
			'top:0;' +
			'left:0;' +
			'overflow:hidden;' +
			'padding:8px 0;' +
			'font:18px/26px Arial,sans-serif;' +
			'text-align:center;' +
			'color:#FFF' +
		'}' +

		'#cloudflare-old-browser a {' +
			'text-decoration:underline;' +
			'color:#EBEBF4' +
		'}' +

		'#cloudflare-old-browser a:hover, #cloudflare-old-browser a:active {' +
			'text-decoration:none;' +
			'color:#DBDBEB' +
		'}' +

		'#cloudflare-old-browser-close {' +
			'background:#393b40;' +
			'display:block;' +
			'width:42px;' +
			'height:42px;' +
			'position:absolute;' +
			'text-decoration:none !important;' +
			'cursor:pointer;' +
			'top:0;' +
			'right:0;' +
			'font-size:30px;' +
			'line-height:42px' +
		'}' +

		'#cloudflare-old-browser-close:hover {' +
			'background:#E04343;' +
			'color:#FFF' +
		'}',

	/**
	 * Have a single var statement, so it compiles better
	 */
	body = doc.body,
	head = doc.getElementsByTagName( 'head' )[ 0 ],
	message = doc.createElement( 'div' ),
	closeButton = doc.createElement( 'a' ),

	/**
	 * Injects style rules into the document to handle formatting
	 */
	style = doc.createElement( 'style' );
	style.id = 'cloudflare-abetterbrowser';
	style.setAttribute( 'type', 'text/css' );

	if( style.styleSheet )
	{
		style.styleSheet.cssText = rules;
	}
	else
	{
		style.appendChild( doc.createTextNode( rules ) );
	}

	head.insertBefore( style, head.firstChild );

	/**
	 * Message container
	 */
	message.id = 'cloudflare-old-browser';
	message.innerHTML = translations[ language ] || translations[ language.substring( 0, 2 ) ] || translations.en;

	/**
	 * Close button
	 */
	closeButton.id = 'cloudflare-old-browser-close';
	closeButton.innerHTML = '×';

	message.appendChild( closeButton );

	closeButton.onclick = function( )
	{
		body.removeChild( message );
		body.className = body.className.replace( new RegExp( '(\\s|^)cloudflare-old-browser-body(\\s|$)' ), '' );

		/**
		 * Hide message for 7 days
		 */
		user.getCookie( 'cfapp_abetterbrowser', 1, 7 );

		return false;
	};

	/**
	 * Injects our message into the body
	 */
	body.appendChild( message );
	body.className += ' cloudflare-old-browser-body';
} );
<commit_msg>Make sure language is in lowercase<commit_after>// View on GitHub: https://github.com/xPaw/CF-ABetterBrowser

CloudFlare.define( 'abetterbrowser', [ 'cloudflare/dom', 'cloudflare/user', 'abetterbrowser/config' ], function( version, user, config )
{
	"use strict";

	version = version.internetExplorer;
	var maxVersion = parseInt( config.ie, 10 ) || 8;

	/**
	 * Is user using IE? Is it old? Did user close this message already?
	 */
	if( version === undefined || version > maxVersion || user.getCookie( 'cfapp_abetterbrowser' ) === 1 )
	{
		return true;
	}

	/**
	 * Let's collect some referral statistics with link shortener service
	 */
	var moreInformationLink = 'http://goo.gl/OVoHF',

	doc = document,

	/**
	 * Detect user's browser language
	 *
	 * This is aimed at IE, so we don't try to get navigator.language
	 */
	language = (window.navigator.browserLanguage || 'en').toLowerCase(),

	/**
	 * Translations
	 */
	translations =
	{
		'en': 'You are using an outdated browser. More information »',
		'is': 'Þú ert að nota úreltan vafra. Nánari upplýsingar »',
		'ru': 'Вы используете устаревший браузер. Подробнее »',
		'fr': 'Votre navigateur n\'est pas à jour. Plus d\'information »',
		'da': 'Du bruger en ældre browser. Mere information »',
		'hu': 'A böngészője elavult. További információ »',
		'de': 'Sie benutzen einen veralteten Browser. Mehr Informationen »',
		'ja': 'あなたは、時代遅れのブラウザを使用しています。詳細情報 »',
		'fa': 'شما از مرورگری قدیمی استفاده می کنید. »اطلاعات بیشتر',
		'tr': 'Çok eski bir tarayıcı kullanıyorsunuz. Daha fazla bilgi »',
		'pt': 'Você está usando um navegador antigo. Mais informações »',
		'pl': 'Używasz przestarzałej przeglądarki. Więcej informacji »',
		'fi': 'Käytät vanhentunutta selainta. Lisää tietoa »',
		'zh': '您的浏览器版本过旧。 更多信息 »',
		'es': 'Su navegador está obsoleto. Más información »',
		'nl': 'U gebruikt op dit moment een verouderde webbrowser. Meer informatie »',
		'vi': 'Trình duyệt bạn dùng đã lỗi thời rồi. Thêm thông tin »',
		'he': 'דפדפן האינטרנט שלך אינו מעודכן. למידע נוסף »',
		'it': 'Stai usando un browser datato. Ulteriori informazioni »',
		'el': 'Χρησιμοποιείτε ένα ξεπερασμένο πρόγραμμα περιήγησης. Περισσότερες πληροφορίες »',
		'id': 'Anda menggunakan web browser versi lama. Informasi selengkapnya »',
		'sr': 'Vi koristite zastarelu verziju browsera. Više informacija »',
		'zh-tw': '你正在使用過時的瀏覽器。 更多訊息 »'
	},

	/**
	 * Style rules
	 */
	rules =
		'#cloudflare-old-browser {' +
			'background:#45484d;' +
			'position:absolute;' +
			'z-index:100000;' +
			'width:100%;' +
			'height:26px;' +
			'top:0;' +
			'left:0;' +
			'overflow:hidden;' +
			'padding:8px 0;' +
			'font:18px/26px Arial,sans-serif;' +
			'text-align:center;' +
			'color:#FFF' +
		'}' +

		'#cloudflare-old-browser a {' +
			'text-decoration:underline;' +
			'color:#EBEBF4' +
		'}' +

		'#cloudflare-old-browser a:hover, #cloudflare-old-browser a:active {' +
			'text-decoration:none;' +
			'color:#DBDBEB' +
		'}' +

		'#cloudflare-old-browser-close {' +
			'background:#393b40;' +
			'display:block;' +
			'width:42px;' +
			'height:42px;' +
			'position:absolute;' +
			'text-decoration:none !important;' +
			'cursor:pointer;' +
			'top:0;' +
			'right:0;' +
			'font-size:30px;' +
			'line-height:42px' +
		'}' +

		'#cloudflare-old-browser-close:hover {' +
			'background:#E04343;' +
			'color:#FFF' +
		'}',

	/**
	 * Have a single var statement, so it compiles better
	 */
	body = doc.body,
	head = doc.getElementsByTagName( 'head' )[ 0 ],
	message = doc.createElement( 'div' ),
	closeButton = doc.createElement( 'a' ),

	/**
	 * Injects style rules into the document to handle formatting
	 */
	style = doc.createElement( 'style' );
	style.id = 'cloudflare-abetterbrowser';
	style.setAttribute( 'type', 'text/css' );

	if( style.styleSheet )
	{
		style.styleSheet.cssText = rules;
	}
	else
	{
		style.appendChild( doc.createTextNode( rules ) );
	}

	head.insertBefore( style, head.firstChild );

	/**
	 * Message container
	 */
	message.id = 'cloudflare-old-browser';
	message.innerHTML = translations[ language ] || translations[ language.substring( 0, 2 ) ] || translations.en;

	/**
	 * Close button
	 */
	closeButton.id = 'cloudflare-old-browser-close';
	closeButton.innerHTML = '×';

	message.appendChild( closeButton );

	closeButton.onclick = function( )
	{
		body.removeChild( message );
		body.className = body.className.replace( new RegExp( '(\\s|^)cloudflare-old-browser-body(\\s|$)' ), '' );

		/**
		 * Hide message for 7 days
		 */
		user.getCookie( 'cfapp_abetterbrowser', 1, 7 );

		return false;
	};

	/**
	 * Injects our message into the body
	 */
	body.appendChild( message );
	body.className += ' cloudflare-old-browser-body';
} );
<|endoftext|>