<commit_before>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');
var crypto = require('crypto');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function PairingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    PairingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(PairingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR = 4;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2 = 5;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA = 6;

PairingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

PairingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

PairingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId, slPk, sc, value;
    console.log("PairingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        var sharedSecret = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(sharedSecret, inv, k, c);
                        console.log("derived shared key: ", sharedSecret);

                        console.log("Creating one time challenge...");
                        sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(sc);
                        // todo remove hardcoded challenge
                        sc = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (sc.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, sc);
                        value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending challenge 1: " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        var r = Buffer.concat([this.keys.clPk, slPk, sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var a = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", a);

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_AUTHENTICATOR) {
                        var clCr = data.slice(2, data.length - 2);
                        console.log("CL authorization authenticator", clCr);

                        // create authenticator with data from server side
                        r = Buffer.concat([this.keys.slPk, this.keys.clPk, sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var cr = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", cr);

                        // Step 14: verify authenticator
                        if (Buffer.compare(clCr, cr)) {
                            console.log("Authenticators verifed ok");
                            // Step 15: send second challenge
                            console.log("Creating second one time challenge...");
                            sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                            sodium.api.randombytes_buf(sc);
                            // todo remove hardcoded challenge
                            sc = new Buffer("E0742CFEA39CB46109385BF91286A3C02F40EE86B0B62FC34033094DE41E2C0D", 'hex');
                            if (sc.length != nukiConstants.NUKI_NONCEBYTES) {
                                console.log("Nonce length (" + sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                                this.state = this.PAIRING_IDLE;
                                callback(this.RESULT_UNLIKELY_ERROR);
                                return;
                            }
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2;
                            this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, sc);
                            value = this.getNextChunk(this.dataStillToSend);
                            if (this._updateValueCallback && value.length > 0) {
                                console.log("sending challenge 2: " + value.length + " bytes");
                                this._updateValueCallback(value);
                            }

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("CL and SL authenticators are not equal. Possible man in the middle attack. Exiting.");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_DATA) {
                        var clAuthData = data.slice(2, data.length - 2);
                        console.log("CL sent authorization data", clAuthData);

                        // todo verify authenticator
                        // todo store new user and generate new authorization-id
                        // todo send new authorization-id

                        // todo continue with state machine

                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('PairingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('PairingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

PairingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("PairingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            var value;
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending PK: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 1: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR;
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 2: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA;
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = PairingGeneralDataInputOutputCharacteristic;
<commit_msg>keys fix<commit_after>var util = require('util');
var nukiConstants = require('./nuki-constants');
var _ = require('underscore');
var crc = require('crc');
var sodium = require('sodium');
var HSalsa20 = require('./hsalsa20');
var crypto = require('crypto');

var bleno = require('bleno');
var BlenoCharacteristic = bleno.Characteristic;
var BlenoDescriptor = bleno.Descriptor;

function PairingGeneralDataInputOutputCharacteristic(keys) {
    this.state = this.PAIRING_IDLE;
    this.keys = keys;
    this.dataStillToSend = new Buffer(0);

    PairingGeneralDataInputOutputCharacteristic.super_.call(this, {
        // uuid: 'a92ee101-5501-11e4-916c-0800200c9a66',
        uuid: 'a92ee101550111e4916c0800200c9a66',
        properties: ['write', 'indicate'],
        descriptors: [
            new BlenoDescriptor({
                uuid: '2902',   // client characterstic configuration
                value: 'Pairing commands'
            })
        ]
    });
}

util.inherits(PairingGeneralDataInputOutputCharacteristic, BlenoCharacteristic);

PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE = 0;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY = 1;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY = 2;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE = 3;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR = 4;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2 = 5;
PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA = 6;

PairingGeneralDataInputOutputCharacteristic.prototype.getNextChunk = function (data) {
    var block0;
    if (data.length > 20) {
        block0 = data.slice(0, 20);
        this.dataStillToSend = data.slice(20);
    } else {
        block0 = data;
        this.dataStillToSend = new Buffer(0);
    }
    return block0;
};

PairingGeneralDataInputOutputCharacteristic.prototype.prepareDataToSend = function (cmd, data) {
    var cmdBuffer = new Buffer(2);
    cmdBuffer.writeUInt16LE(cmd);
    var responseData = Buffer.concat([cmdBuffer, data]);
    var checksum = crc.crc16ccitt(responseData);
    var checksumBuffer = new Buffer(2);
    checksumBuffer.writeUInt16LE(checksum);
    this.dataStillToSend = Buffer.concat([responseData, checksumBuffer]);
    console.log("prepared to send:", this.dataStillToSend);
};

PairingGeneralDataInputOutputCharacteristic.prototype.onWriteRequest = function (data, offset, withoutResponse, callback) {
    var cmdId, slPk, sc, value;
    console.log("PairingGeneralDataInputOutputCharacteristic", data);
    var dataForCrc = data.slice(0, data.length - 2);
    var crcSumCalc = crc.crc16ccitt(dataForCrc);
    var crcSumRetrieved = data.readUInt16LE(data.length - 2);

    if (crcSumCalc === crcSumRetrieved) {
        console.log("checksum is ok");
        if (offset) {
            callback(this.RESULT_ATTR_NOT_LONG);
        } else if (data.length > 200) {
            callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH);
        } else {
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_IDLE:
                    var rCmd = data.readUInt16LE(0);
                    cmdId = data.readUInt16LE(2);
                    if (rCmd === nukiConstants.CMD_reqUEST_DATA && cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }

                        if (slPk.length > 0) {
                            this.prepareDataToSend(nukiConstants.CMD_ID_PUBLIC_KEY, slPk);
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY;

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("ERROR missing SL public key");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_ID_PUBLIC_KEY) {
                        this.keys.clPk = data.slice(2, data.length - 2);
                        console.log("CL PUBKEY:", this.keys.clPk);

                        slPk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slPk)) {
                            slPk = this.keys.slPk;
                        } else {
                            if (_.isString(this.keys.slPk)) {
                                slPk = new Buffer(this.keys.slPk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slPk)) {
                                    slPk = new Buffer(this.keys.slPk);
                                }
                            }
                        }
                        var slSk = new Buffer(0);
                        if (Buffer.isBuffer(this.keys.slSk)) {
                            slSk = this.keys.slSk;
                        } else {
                            if (_.isString(this.keys.slSk)) {
                                slSk = new Buffer(this.keys.slSk, 'hex');
                            } else {
                                if (_.isArray(this.keys.slSk)) {
                                    slSk = new Buffer(this.keys.slSk);
                                }
                            }
                        }

                        this.keys.slPk = slPk;
                        this.keys.slSk = slSk;

                        console.log("slPK", slPk);
                        console.log("slSK", slSk);
                        console.log("clPK", this.keys.clPk);

                        // Create Diffie-Hellman key from nuki secret key and clients public key
                        // crypto_scalarmult_curve25519(k,secretKey,pk)
                        var k = sodium.api.crypto_scalarmult(slSk, this.keys.clPk);
                        console.log("SL DH Key from SL SK and CL PK: ", k);

                        // derive a longterm shared secret key s from k using function kdf1
                        // static const unsigned char _0[16];
                        // static const unsigned char sigma[16] = "expand 32-byte k";
                        // crypto_core_hsalsa20(firstKey,_0,sharedKey,sigma)
                        var hsalsa20 = new HSalsa20();
                        var sharedSecret = new Buffer(32);
                        var inv = new Buffer(16);
                        inv.fill(0);
                        var c = new Buffer("expand 32-byte k");
                        hsalsa20.crypto_core(sharedSecret, inv, k, c);
                        console.log("derived shared key: ", sharedSecret);

                        console.log("Creating one time challenge...");
                        sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                        sodium.api.randombytes_buf(sc);
                        // todo remove hardcoded challenge
                        sc = new Buffer("6CD4163D159050C798553EAA57E278A579AFFCBC56F09FC57FE879E51C42DF17", 'hex');
                        if (sc.length != nukiConstants.NUKI_NONCEBYTES) {
                            console.log("Nonce length (" + sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                            return;
                        }

                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE;
                        this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, sc);
                        value = this.getNextChunk(this.dataStillToSend);
                        if (this._updateValueCallback && value.length > 0) {
                            console.log("sending challenge 1: " + value.length + " bytes");
                            this._updateValueCallback(value);
                        }

                        var r = Buffer.concat([this.keys.clPk, slPk, sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var a = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", a);

                        callback(this.RESULT_SUCCESS);
                    }
                    else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_AUTHENTICATOR) {
                        var clCr = data.slice(2, data.length - 2);
                        console.log("CL authorization authenticator", clCr);

                        // create authenticator with data from server side
                        r = Buffer.concat([this.keys.slPk, this.keys.clPk, sc]);
                        // use HMAC-SHA256 to create the authenticator
                        var cr = crypto.createHmac('SHA256', sharedSecret).update(r).digest();
                        console.log("SL Authorization authenticator", cr);

                        // Step 14: verify authenticator
                        if (Buffer.compare(clCr, cr)) {
                            console.log("Authenticators verifed ok");
                            // Step 15: send second challenge
                            console.log("Creating second one time challenge...");
                            sc = new Buffer(nukiConstants.NUKI_NONCEBYTES);
                            sodium.api.randombytes_buf(sc);
                            // todo remove hardcoded challenge
                            sc = new Buffer("E0742CFEA39CB46109385BF91286A3C02F40EE86B0B62FC34033094DE41E2C0D", 'hex');
                            if (sc.length != nukiConstants.NUKI_NONCEBYTES) {
                                console.log("Nonce length (" + sc.length + ") is not " + nukiConstants.NUKI_NONCEBYTES);
                                this.state = this.PAIRING_IDLE;
                                callback(this.RESULT_UNLIKELY_ERROR);
                                return;
                            }
                            this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2;
                            this.prepareDataToSend(nukiConstants.CMD_CHALLENGE, sc);
                            value = this.getNextChunk(this.dataStillToSend);
                            if (this._updateValueCallback && value.length > 0) {
                                console.log("sending challenge 2: " + value.length + " bytes");
                                this._updateValueCallback(value);
                            }

                            callback(this.RESULT_SUCCESS);
                        } else {
                            console.log("CL and SL authenticators are not equal. Possible man in the middle attack. Exiting.");
                            this.state = this.PAIRING_IDLE;
                            callback(this.RESULT_UNLIKELY_ERROR);
                        }
                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA:
                    cmdId = data.readUInt16LE(0);
                    if (cmdId === nukiConstants.CMD_AUTHORIZATION_DATA) {
                        var clAuthData = data.slice(2, data.length - 2);
                        console.log("CL sent authorization data", clAuthData);

                        // todo verify authenticator
                        // todo store new user and generate new authorization-id
                        // todo send new authorization-id

                        // todo continue with state machine

                    } else {
                        console.log("command or command identifier wrong");
                        this.state = this.PAIRING_IDLE;
                        callback(this.RESULT_UNLIKELY_ERROR);
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
                    callback(this.RESULT_UNLIKELY_ERROR);
            }
        }
    } else {
        console.log("checksum is NOT ok");
        this.state = this.PAIRING_IDLE;
        callback(this.RESULT_UNLIKELY_ERROR);
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onSubscribe = function (maxValueSize, updateValueCallback) {
    console.log('PairingGeneralDataInputOutputCharacteristic - onSubscribe');

    this._updateValueCallback = updateValueCallback;

    if (this.dataStillToSend.length > 0) {
        switch (this.state) {
            case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:

                var value = this.getNextChunk(this.dataStillToSend);
                if (value.length > 0) {
                    console.log("sending " + value.length + " bytes from onSubscribe");
                    updateValueCallback(value);
                }

                if (this.dataStillToSend.length === 0) {
                    this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                }

                break;
            default:
                console.log("ERROR unexpected pairing state");
                this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to notify");
    }
};

PairingGeneralDataInputOutputCharacteristic.prototype.onUnsubscribe = function () {
    console.log('PairingGeneralDataInputOutputCharacteristic - onUnsubscribe');

    this._updateValueCallback = null;
};

PairingGeneralDataInputOutputCharacteristic.prototype.onIndicate = function () {
    console.log("PairingGeneralDataInputOutputCharacteristic indicate");
    if (this.dataStillToSend.length > 0) {
        if (this._updateValueCallback) {
            var value;
            switch (this.state) {
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_PUBKEY:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending PK: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_PUBKEY;
                    }

                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 1: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHENTICATOR;
                    }
                    break;
                case PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_SL_SEND_CHALLENGE_2:
                    value = this.getNextChunk(this.dataStillToSend);
                    if (value.length > 0) {
                        console.log("sending challenge 2: " + value.length + " bytes as indication");
                        this._updateValueCallback(value);
                    }
                    if (this.dataStillToSend.length === 0) {
                        this.state = PairingGeneralDataInputOutputCharacteristic.prototype.PAIRING_CL_SEND_AUTHORIZATION_DATA;
                    }
                    break;
                default:
                    console.log("ERROR unexpected pairing state");
                    this.state = this.PAIRING_IDLE;
            }
        } else {
            console.log("don't have updateValueCallback on indicate");
            this.state = this.PAIRING_IDLE;
        }
    } else {
        console.log("don't have more data to indicate");
    }
};

module.exports = PairingGeneralDataInputOutputCharacteristic;
<|endoftext|>


<commit_before>'use strict';
var async = require('async');
var BattleNet = require('battlenet-api');
var plugins = require.main.require('./src/plugins');
var db = require.main.require('./src/database');
var privileges = require.main.require('./src/privileges');
var meta = require.main.require("./src/meta");
var Roster = {};

var isValidRank = function (id) {
  switch (id) {
    case 0:
      return true;//"Guild Master";
    case 1:
      return true;//"Officer";
    case 2:
      return false;//"Officer Alt";
    case 3:
      return true;//"Raider";
    case 4:
      return true;//"Trial";
    case 5:
      return true;//"Raid Alt";
    case 6:
      return false;//"Alt";
    case 7:
      return false;//"Casual";
  }
  return false;
}

Roster.getRoster = function (callback) {

  db.getObject('roster', (err, data) => {
    callback(JSON.parse(data.members));
  });
};

Roster.updateRoster = function (uid, callback) {
		var timestamp = Date.now();
  var guild;
  var realm;
  var region;
  var bnet;
  var validCharacters;
  async.waterfall([
    (next) => {
      privileges.users.isAdministrator(uid, next);
    },
    (isAdmin, next) => {
       meta.settings.get("moonlight", next);
    },
    (settings, next) => {
      bnet = BattleNet(settings.key);
      region = settings.region;
      realm = settings.realm;
      guild = settings.guild;

      bnet.wow.guild.members({ origin: region, realm: realm, name: guild }, (err, data, res) => {
        next(null, data);
      });
    },
    (data, next) => {
      if (!data || !Array.isArray(data.members)) {
          return next(new Error(JSON.stringify(data)));
        }
        validCharacters = data.members.filter((x, i) => x.character.level === 110);//data.members.filter(x => isValidRank(x.rank));//
        console.log("Requesting details for " + validCharacters.length + " characters")
        var calls = validCharacters.map(x => {
          return (cb) => {
            bnet.wow.character.aggregate({ origin: region, realm: x.character.realm, name: x.character.name, fields: ["items", "achievements"] }, cb);
          }
        });

        console.log(calls.length);
        async.parallelLimit(calls, 50, next);
    },
    (data, next) => {
          var characterDetails = data.map(x => x[0]).filter(x => !!x && x.status !== "nok");
          var returnData = characterDetails.filter(x => !!x)
            .map((x, i) => {
              // RETRIEVE ACHIEVEMENT STATUS CLASS HALL BELT -> CRITERIA QUANTITS VALUE = TOTAL AP GAINED
              var totalAP = 0;
              if (x.achievements && x.achievements.criteria) {
                var classHallBeltAchIndex = x.achievements.criteria.indexOf(30103);
                totalAP = x.achievements.criteriaQuantity[classHallBeltAchIndex];
              }
              return {
                name: x.name,
                lastModified: x.lastModified,
                realm: x.realm,
                "class": x.class,
                race: x.race,
                level: x.level,
                thumbnail: x.thumbnail,
                rank: validCharacters[i].rank,
                averageItemLevelEquipped: x.items.averageItemLevelEquipped,
                audit: itemAudit(x.items),
                totalArtifactPower: totalAP
              };
            });
          db.setObject('roster', { members: JSON.stringify(returnData) }, next);
    }
  ], (err, results) => {
    console.log("Done!");
    if (err) {
      return callback(err);
    } else {
      return callback(null);
    }
  });

}

var hasGemSlot = function (item) {
  return item["tooltipParams"]["gem0"]
}

var itemAudit = function (items) {
  var finger1 = items["finger1"];
  var finger2 = items["finger2"];
  var back = items["back"];
  var neck = items["neck"];
  var mainHand = items["mainHand"];
  var offHand = items["offHand"];
  var missingEnchants = [];
  var missingGems = [];
  var socketCount = 0;
  var setItems = [];
  var missingUpgrades = [];

  for (i in items) {
    var item = items[i];
    if (item["tooltipParams"]) {
      var tooltip = item["tooltipParams"];
      if (tooltip["set"] && setItems.length == 0) {
        setItems = tooltip["set"];
      }

      var gem = hasGemSlot(item)
      if (gem) {
        socketCount++;
        if (!validGem(gem)) {
          missingGems.push(i);
        }
      }

      //   var upgradable = tooltip["upgrade"]
      //   if(upgradable){
      //     if(!isUpgraded(upgradable)){
      //       missingUpgrades.push(i)
      //     }
      //   }

    }
  }
  //   if (!enchant(finger1) || !validFingerEnchant(enchant(finger1))) {
  //     missingEnchants.push("finger1")
  //   }
  //   if (!enchant(finger2) || !validFingerEnchant(enchant(finger2))) {
  //     missingEnchants.push("finger2")
  //   }
  //   if (!enchant(back) || !validBackEnchant(enchant(back))) {
  //     missingEnchants.push("back")
  //   }
  //   if (!enchant(neck) || !validNeckEnchant(enchant(neck))) {
  //     missingEnchants.push("neck")
  //   }
  //   if (!enchant(mainHand) || !validWeaponEnchant(enchant(mainHand))) {
  //     missingEnchants.push("mainHand")
  //   }
  //   if (offHand && offHand["weaponInfo"] && (!enchant(offHand) || !validWeaponEnchant(enchant(offHand)))) {
  //     missingEnchants.push("offHand")
  //   }
  var enchantsPassed = "E:" + (missingEnchants.length == 0 || missingEnchants[0] == null ? "passed" : missingEnchants.join(','))
  var gemsPassed = "G:" + (missingGems.length == 0 || missingGems[0] == null ? "passed" : missingGems.join(','))
  var upgradesPassed = "U:" + (missingUpgrades.length == 0 || missingUpgrades[0] == null ? "passed" : missingUpgrades.join(','))

  var traitCount = 0;
  var artifactIlvl = 0;
  if (items.mainHand && items.mainHand.artifactTraits) {
    traitCount = items.mainHand.artifactTraits.reduce((sum, x) => sum + x.rank, 0);
    artifactIlvl = items.mainHand.itemLevel;
  }
  if (traitCount === 0 && items.offHand && items.offHand.artifactTraits) {
    //Check offhand for tanks
    traitCount = items.offHand.artifactTraits.reduce((sum, x) => sum + x.rank, 0);
    artifactIlvl = items.offHand.itemLevel;
  }

  return {
    sockets: {
      count: socketCount
    },
    set: {
      parts: setItems
    },
    artifact: {
      traitCount: traitCount,
      itemLevel: artifactIlvl
    }
  };
}

var validGem = function (id) {
  if (id > 0) {
    return true;
  }
  if ((id >= 127760 && id <= 127765)) {
    return true
  }
  return false
}

module.exports = Roster;

<commit_msg>- 3 for now on trait count<commit_after>'use strict';
var async = require('async');
var BattleNet = require('battlenet-api');
var plugins = require.main.require('./src/plugins');
var db = require.main.require('./src/database');
var privileges = require.main.require('./src/privileges');
var meta = require.main.require("./src/meta");
var Roster = {};

var isValidRank = function (id) {
  switch (id) {
    case 0:
      return true;//"Guild Master";
    case 1:
      return true;//"Officer";
    case 2:
      return false;//"Officer Alt";
    case 3:
      return true;//"Raider";
    case 4:
      return true;//"Trial";
    case 5:
      return true;//"Raid Alt";
    case 6:
      return false;//"Alt";
    case 7:
      return false;//"Casual";
  }
  return false;
}

Roster.getRoster = function (callback) {

  db.getObject('roster', (err, data) => {
    callback(JSON.parse(data.members));
  });
};

Roster.updateRoster = function (uid, callback) {
		var timestamp = Date.now();
  var guild;
  var realm;
  var region;
  var bnet;
  var validCharacters;
  async.waterfall([
    (next) => {
      privileges.users.isAdministrator(uid, next);
    },
    (isAdmin, next) => {
       meta.settings.get("moonlight", next);
    },
    (settings, next) => {
      bnet = BattleNet(settings.key);
      region = settings.region;
      realm = settings.realm;
      guild = settings.guild;

      bnet.wow.guild.members({ origin: region, realm: realm, name: guild }, (err, data, res) => {
        next(null, data);
      });
    },
    (data, next) => {
      if (!data || !Array.isArray(data.members)) {
          return next(new Error(JSON.stringify(data)));
        }
        validCharacters = data.members.filter((x, i) => x.character.level === 110);//data.members.filter(x => isValidRank(x.rank));//
        console.log("Requesting details for " + validCharacters.length + " characters")
        var calls = validCharacters.map(x => {
          return (cb) => {
            bnet.wow.character.aggregate({ origin: region, realm: x.character.realm, name: x.character.name, fields: ["items", "achievements"] }, cb);
          }
        });

        console.log(calls.length);
        async.parallelLimit(calls, 50, next);
    },
    (data, next) => {
          var characterDetails = data.map(x => x[0]).filter(x => !!x && x.status !== "nok");
          var returnData = characterDetails.filter(x => !!x)
            .map((x, i) => {
              // RETRIEVE ACHIEVEMENT STATUS CLASS HALL BELT -> CRITERIA QUANTITS VALUE = TOTAL AP GAINED
              var totalAP = 0;
              if (x.achievements && x.achievements.criteria) {
                var classHallBeltAchIndex = x.achievements.criteria.indexOf(30103);
                totalAP = x.achievements.criteriaQuantity[classHallBeltAchIndex];
              }
              return {
                name: x.name,
                lastModified: x.lastModified,
                realm: x.realm,
                "class": x.class,
                race: x.race,
                level: x.level,
                thumbnail: x.thumbnail,
                rank: validCharacters[i].rank,
                averageItemLevelEquipped: x.items.averageItemLevelEquipped,
                audit: itemAudit(x.items),
                totalArtifactPower: totalAP
              };
            });
          db.setObject('roster', { members: JSON.stringify(returnData) }, next);
    }
  ], (err, results) => {
    console.log("Done!");
    if (err) {
      return callback(err);
    } else {
      return callback(null);
    }
  });

}

var hasGemSlot = function (item) {
  return item["tooltipParams"]["gem0"]
}

var itemAudit = function (items) {
  var finger1 = items["finger1"];
  var finger2 = items["finger2"];
  var back = items["back"];
  var neck = items["neck"];
  var mainHand = items["mainHand"];
  var offHand = items["offHand"];
  var missingEnchants = [];
  var missingGems = [];
  var socketCount = 0;
  var setItems = [];
  var missingUpgrades = [];

  for (i in items) {
    var item = items[i];
    if (item["tooltipParams"]) {
      var tooltip = item["tooltipParams"];
      if (tooltip["set"] && setItems.length == 0) {
        setItems = tooltip["set"];
      }

      var gem = hasGemSlot(item)
      if (gem) {
        socketCount++;
        if (!validGem(gem)) {
          missingGems.push(i);
        }
      }

      //   var upgradable = tooltip["upgrade"]
      //   if(upgradable){
      //     if(!isUpgraded(upgradable)){
      //       missingUpgrades.push(i)
      //     }
      //   }

    }
  }
  //   if (!enchant(finger1) || !validFingerEnchant(enchant(finger1))) {
  //     missingEnchants.push("finger1")
  //   }
  //   if (!enchant(finger2) || !validFingerEnchant(enchant(finger2))) {
  //     missingEnchants.push("finger2")
  //   }
  //   if (!enchant(back) || !validBackEnchant(enchant(back))) {
  //     missingEnchants.push("back")
  //   }
  //   if (!enchant(neck) || !validNeckEnchant(enchant(neck))) {
  //     missingEnchants.push("neck")
  //   }
  //   if (!enchant(mainHand) || !validWeaponEnchant(enchant(mainHand))) {
  //     missingEnchants.push("mainHand")
  //   }
  //   if (offHand && offHand["weaponInfo"] && (!enchant(offHand) || !validWeaponEnchant(enchant(offHand)))) {
  //     missingEnchants.push("offHand")
  //   }
  var enchantsPassed = "E:" + (missingEnchants.length == 0 || missingEnchants[0] == null ? "passed" : missingEnchants.join(','))
  var gemsPassed = "G:" + (missingGems.length == 0 || missingGems[0] == null ? "passed" : missingGems.join(','))
  var upgradesPassed = "U:" + (missingUpgrades.length == 0 || missingUpgrades[0] == null ? "passed" : missingUpgrades.join(','))

  var traitCount = 0;
  var artifactIlvl = 0;
  if (items.mainHand && items.mainHand.artifactTraits) {
    traitCount = items.mainHand.artifactTraits.reduce((sum, x) => sum + x.rank, 0) - 3; //This should reduce by number of relics instead of static 3
    artifactIlvl = items.mainHand.itemLevel;
  }
  if (traitCount < 1 && items.offHand && items.offHand.artifactTraits) {
    //Check offhand for tanks && warlocks
    traitCount = items.offHand.artifactTraits.reduce((sum, x) => sum + x.rank, 0) - 3;
    artifactIlvl = items.offHand.itemLevel;
  }

  return {
    sockets: {
      count: socketCount
    },
    set: {
      parts: setItems
    },
    artifact: {
      traitCount: traitCount,
      itemLevel: artifactIlvl
    }
  };
}

var validGem = function (id) {
  if (id > 0) {
    return true;
  }
  if ((id >= 127760 && id <= 127765)) {
    return true
  }
  return false
}

module.exports = Roster;

<|endoftext|>


<commit_before>var _ = require('lodash');
var uploader = require('./middlewares/uploader');
var hooks = require('./hooks');

var router = {
  routeMap: {}
};

/**
 * Bind one we.js route
 *
 * This function bind context loader, acl, upload and controller middlewares
 *
 * @param  {Object} we     we.js object
 * @param  {String} route  route like "get /route"
 * @param  {Object} config route configs
 */
router.bindRoute = function bindRoute(we, route, config) {
  var method, path;
  // parse method and url
  var r = route.split(' ');
  if (r.length > 1) {
    method = r[0];
    path = r[1];
  } else {
    method = 'get';
    path = r[0];
  }

  if (config.method) {
    method = config.method;
  }

  var actionFunction = '';
  // search for the controller action to bind
  if (
    we.controllers[config.controller] &&
    we.controllers[config.controller][config.action]
  ) {
    actionFunction = we.controllers[config.controller][config.action];
  } else {
    return we.log.warn('we.router.bindRoute: Unknow controller or action:', path, config);
  }

  var middlewares = [
    // bind context loader
    router.contextLoader.bind({
      config: config
    }),
    // bind acl middleware
    we.acl.canMiddleware.bind({
      config: config
    })
  ];

  we.events.emit('router:add:acl:middleware', {
    we: we, middlewares: middlewares, config: config
  });

  // bind upload  if have config and after ACL check
  if (config.upload) {
    middlewares.push(uploader(config.upload));
  }

  we.events.emit('router:before:set:controller:middleware', {we: we, middlewares: middlewares, config: config});
  // bind contoller
  middlewares.push(actionFunction);

  we.express[method](path, middlewares);

  var mapName = config.name;
  if (!mapName) {
    mapName = config.controller + '.' + config.action;
  }

  if (!we.router.routeMap[method]) we.router.routeMap[method] = {};

  // map get routes for use with link-to
  we.router.routeMap[method][mapName] = {
    map: router.parseRouteToMap(path),
    config: config
  };

  we.log.silly('Route bind:', method ,path);
}

// absolute url regex tester
var absoluteUrlRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
/**
 * Check if one url is absolute or relative
 *
 * @param  {String}  str url to check
 * @return {Boolean}     Returns true for absolute and false to relative
 */
router.isAbsoluteUrl = function isAbsoluteUrl(str) {
  return absoluteUrlRegex.test(str);
}

router.parseRouteToMap = function parseRouteToMap(route) {
  return route.split('/').map(function(r, i){
    if(r[0] === ':'){
      return { name: r, i: i };
    } else {
      return r;
    }
  });
}

router.urlTo = function urlTo(name, params ,we) {
  var url = '';
  var route = {};

  if (we.router.routeMap.get[name]) {
    route = we.router.routeMap.get[name];
    var mapI = 0;
    for (var i = 0; i < route.map.length; i++) {
      // skip empty path parts linke  // and fist /
      if (!route.map[i]) continue;
      if (typeof route.map[i] == 'string') {
        url += '/' + route.map[i];
      } else if (params && params[mapI]){
        url += '/' + params[mapI];
        mapI++;
      } else {
        we.log.warn('Invalid or undefined argument: ' + params +' ', route.map[i]);
      }
    }
  } else {
    we.log.warn('Route map not found: ' + name);
  }

  if (route.map && route.map.length && !url) url = '/';

  return url;
}

/**
 * Context loader middleware
 */
router.contextLoader = function contextLoader(req, res, next) {
  // only load context one time per request
  if (req.weContextLoaded) return next();

  var we = req.getWe();
  // save current user reference
  res.locals.currentUser = req.user;
  // get route configs
  var config = this.config;
  // merge context var with route configs
  _.merge(res.locals, config);

  router.parseQuery(req, res, function(err, query) {
    res.locals.query = query;
    if (!res.locals.model ) return contextLoaded();
    // set model class
    res.locals.Model = we.db.models[res.locals.model];
    // set id if exists and not is set
    if (!res.locals.id) {
      if (req.params[res.locals.model + 'Id']) {
        res.locals.id = req.params[res.locals.model + 'Id'];
      } else if (req.params.id) {
        res.locals.id = req.params.id;
      }
    }

    // set load current record flag for single records requests
    res.locals.loadCurrentRecord = router.needLoadCurrentModel(req, res);
    // default template
    if (!res.locals.template)
      res.locals.template = res.locals.model + '/' + res.locals.action;

    // run model context loader if exists
    if (
      we.db.models[res.locals.model] &&
      typeof we.db.models[res.locals.model].contextLoader == 'function'
    ) {
      we.db.models[res.locals.model].contextLoader(req, res, function afterRunModelContextLoader(err) {
        return contextLoaded(err);
      });
    } else {
      return contextLoaded();
    }

    function contextLoaded(err) {
      req.weContextLoaded = true;
      if (err) return next(err);

      hooks.trigger('we:router:request:after:load:context', {
        req: req, res: res
      }, function (err) {
        if (err) return res.serverError(err);
        if (!req.user) return next();
        // preload user roles
        return req.user.getRoles().then(function (result) {
          next(null, result);
        });
      });
    }
  });
}

/**
 * We.js title middleware handlers
 */
router.title = {
  middleware: function titleMiddleware(data, next) {
    if (data.res.locals.titleHandler) {
      if (typeof data.res.locals.titleHandler === 'function') {
        return data.res.locals.titleHandler(data.req, data.res, next);
      } else if (router.title.middlewares[data.res.locals.titleHandler]) {
        return router.title.middlewares[data.res.locals.titleHandler](data.req, data.res, next);
      }
    }

    next();
  },
  middlewares: {
    i18n: function i18n(req, res, next) {
      res.locals.title = req.__(res.locals.titleI18n);
      next();
    },

    recordField: function recordField(req, res, next) {
      if (res.locals.record)
        res.locals.title = res.locals.record[res.locals.titleField];

      next();
    }
  }
}
hooks.on('we:router:request:after:load:context', router.title.middleware);

/**
 * Add one route after 404 middleware
 */
router.liveBindRoute = function bindRoute(we, route, config) {
  var allRoutes = we.express._router.stack;
  we.router.bindRoute(we, route, config);
  // push this new route to after 404 and error middlewares
  allRoutes.splice(allRoutes.length-3, 0, allRoutes.splice(allRoutes.length-1, 1)[0]);
}

// router.bindShadownRoutes = function bindShadownRoutes(we, cb) {
//   if (!we.db.models || !we.controllers) return cb();

//   var modelNames = Object.keys(we.db.models);

//   var shadownRoutesToBind = [];
//   for (var i = modelNames.length - 1; i >= 0; i--) {
//     if (router.haveShadownRoute(we,  modelNames[i])) {
//       shadownRoutesToBind.push(modelNames[i]);
//     }
//   }

//   shadownRoutesToBind.forEach(function(model) {
//     // get / find
//     router.bindShadownFindRoute(we, model);
//     // post / create
//     router.bindShadownCreateRoute(we, model);
//     // get :id / findOne
//     router.bindShadownFindOneRoute(we, model);
//     // put / update
//     router.bindShadownUpdateRoute(we, model);
//     // delete / destroy
//     router.bindShadownDeleteRoute(we, model);
//   });

//   cb();
// }

/**
 * Check if one model have shadownRoutes
 *
 * @param  {Object} we        We.js object
 * @param  {String} modelName
 * @return {Boolean}
 */
router.haveShadownRoute = function haveShadownRoute(we, modelName) {
  if ( !we.controllers[modelName] || !we.db.models[modelName] ) return false;
  if ( we.controllers[modelName] &&
    we.controllers[modelName]._config &&
    (we.controllers[modelName]._config.shadownRoutes === false)
  ) {
    return false;
  }
  return true;
}

// -- wejs shadown routes bind functions, override if need something diferent

/**
 * Bind find shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownFindRoute = function bindShadownFindRoute(we, model) {
  var configs  = {
    action: 'find',
    model: model,
    controller: model
  };

  var route = '/' + model;

  we.router.bindRoute(we, route, configs);
}

/**
 * Bind create shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownCreateRoute = function bindShadownCreateRoute(we, model) {
  var configs  = {
    method: 'post',
    action: 'create',
    model: model,
    controller: model
  };

  var route = 'post /' + model;

  we.router.bindRoute(we, route, configs);
}

router.bindShadownGenericRoute = function bindShadownGenericRoute(we, model, action, method) {
  var configs  = {
    method: method,
    action: action,
    model: model,
    controller: model
  };

  var route = method + ' /' + model + '/:id';

  we.router.bindRoute(we, route, configs);
}

/**
 * Bind findOne shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownFindOneRoute = function bindShadownFindOneRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'findOne', 'get');
}

/**
 * Bind update shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownUpdateRoute = function bindShadownUpdateRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'update', 'put');
}

/**
 * Bind delete shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownDeleteRoute = function bindShadownDeleteRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'destroy', 'delete');
}

/**
 * Bind updateAtribute shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownUpdateAtributeRoute = function bindShadownUpdateAtributeRoute(we, model) {
  var configs  = {
    method: 'put',
    action: 'updateAtribute',
    model: model,
    controller: model
  };

  var route = 'put /' + model + '/:id/:atribute';
  we.router.bindRoute(we, route, configs);
}

router.singleRecordActions = [
  'findOne', 'update', 'destroy', 'updateAttribute',
  'deleteAttribute', 'addRecord', 'removeRecord', 'getRecord', 'editPage'
];

router.needLoadCurrentModel = function needLoadCurrentModel(req, res) {
  if (res.locals.loadRecord === false) return false;
  if (res.locals.loadRecord === true) return true;

  if (!res.locals.action) return false;

  if ( router.singleRecordActions.indexOf( res.locals.action ) >-1 ) {
    return true;
  }
}

/**
 * Parse query for current request
 */
router.parseQuery = function parseQuery(req, res, next) {
  var we = req.getWe();
  var query = {
    // set subQuery to false by default
    subQuery: false,
    where: {}
  };

  if (res.locals.skipParseQuery) return query;

  if (req.query.offset) {
    query.offset = req.query.offset;
  } else if (req.query.skip) {
    query.offset = req.query.skip;
  }

  if (req.query.limit) {
    query.limit = req.query.limit;
  }

  if (req.query.order) {
    query.order = res.locals.model + '.' +req.query.order;
  } else if (req.query.sort) {
    query.order = res.locals.model + '.' +req.query.sort;
  }

  query.include = [{ all: true,  attributes: ['id'] }];

  // parse where
  try {
    if (req.query.where) query.where = JSON.parse( req.query.where );
  } catch(e) {
    we.log.warn('req.query.where have a invalid format', req.query.where);
    return res.badRequest();
  }

  // parse associations
  if (res.locals.model && we.db.models[res.locals.model]) {
    // we.log.info('Models>',we.db.models[res.locals.model]);

    if (we.db.models[res.locals.model].associations) {
      var assocFields = Object.keys(we.db.models[res.locals.model].associations);
      var identifier;
      for (var i = assocFields.length - 1; i >= 0; i--) {
        identifier = we.db.models[res.locals.model].associations[assocFields[i]].identifier;
        // parse in body params
        if (req.body && req.body[assocFields[i]]) {
          req.body[identifier] = req.body[assocFields[i]];
        }
        // parse in where
        if (query.where && query.where[assocFields[i]]) {
          query.where[identifier] = query.where[assocFields[i]];
        }
      }
    }
  }

  return next(null, query);
}

module.exports = router;
<commit_msg>add default sort and update title middleware<commit_after>var _ = require('lodash');
var uploader = require('./middlewares/uploader');
var hooks = require('./hooks');

var router = {
  routeMap: {}
};

/**
 * Bind one we.js route
 *
 * This function bind context loader, acl, upload and controller middlewares
 *
 * @param  {Object} we     we.js object
 * @param  {String} route  route like "get /route"
 * @param  {Object} config route configs
 */
router.bindRoute = function bindRoute(we, route, config) {
  var method, path;
  // parse method and url
  var r = route.split(' ');
  if (r.length > 1) {
    method = r[0];
    path = r[1];
  } else {
    method = 'get';
    path = r[0];
  }

  if (config.method) {
    method = config.method;
  }

  var actionFunction = '';
  // search for the controller action to bind
  if (
    we.controllers[config.controller] &&
    we.controllers[config.controller][config.action]
  ) {
    actionFunction = we.controllers[config.controller][config.action];
  } else {
    return we.log.warn('we.router.bindRoute: Unknow controller or action:', path, config);
  }

  var middlewares = [
    // bind context loader
    router.contextLoader.bind({
      config: config
    }),
    // bind acl middleware
    we.acl.canMiddleware.bind({
      config: config
    })
  ];

  we.events.emit('router:add:acl:middleware', {
    we: we, middlewares: middlewares, config: config
  });

  // bind upload  if have config and after ACL check
  if (config.upload) {
    middlewares.push(uploader(config.upload));
  }

  we.events.emit('router:before:set:controller:middleware', {we: we, middlewares: middlewares, config: config});
  // bind contoller
  middlewares.push(actionFunction);

  we.express[method](path, middlewares);

  var mapName = config.name;
  if (!mapName) {
    mapName = config.controller + '.' + config.action;
  }

  if (!we.router.routeMap[method]) we.router.routeMap[method] = {};

  // map get routes for use with link-to
  we.router.routeMap[method][mapName] = {
    map: router.parseRouteToMap(path),
    config: config
  };

  we.log.silly('Route bind:', method ,path);
}

// absolute url regex tester
var absoluteUrlRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
/**
 * Check if one url is absolute or relative
 *
 * @param  {String}  str url to check
 * @return {Boolean}     Returns true for absolute and false to relative
 */
router.isAbsoluteUrl = function isAbsoluteUrl(str) {
  return absoluteUrlRegex.test(str);
}

router.parseRouteToMap = function parseRouteToMap(route) {
  return route.split('/').map(function(r, i){
    if(r[0] === ':'){
      return { name: r, i: i };
    } else {
      return r;
    }
  });
}

router.urlTo = function urlTo(name, params ,we) {
  var url = '';
  var route = {};

  if (we.router.routeMap.get[name]) {
    route = we.router.routeMap.get[name];
    var mapI = 0;
    for (var i = 0; i < route.map.length; i++) {
      // skip empty path parts linke  // and fist /
      if (!route.map[i]) continue;
      if (typeof route.map[i] == 'string') {
        url += '/' + route.map[i];
      } else if (params && params[mapI]){
        url += '/' + params[mapI];
        mapI++;
      } else {
        we.log.warn('Invalid or undefined argument: ' + params +' ', route.map[i]);
      }
    }
  } else {
    we.log.warn('Route map not found: ' + name);
  }

  if (route.map && route.map.length && !url) url = '/';

  return url;
}

/**
 * Context loader middleware
 */
router.contextLoader = function contextLoader(req, res, next) {
  // only load context one time per request
  if (req.weContextLoaded) return next();

  var we = req.getWe();
  // save current user reference
  res.locals.currentUser = req.user;
  // get route configs
  var config = this.config;
  // merge context var with route configs
  _.merge(res.locals, config);

  router.parseQuery(req, res, function(err, query) {
    res.locals.query = query;
    if (!res.locals.model ) return contextLoaded();
    // set model class
    res.locals.Model = we.db.models[res.locals.model];
    // set id if exists and not is set
    if (!res.locals.id) {
      if (req.params[res.locals.model + 'Id']) {
        res.locals.id = req.params[res.locals.model + 'Id'];
      } else if (req.params.id) {
        res.locals.id = req.params.id;
      }
    }

    // set load current record flag for single records requests
    res.locals.loadCurrentRecord = router.needLoadCurrentModel(req, res);
    // default template
    if (!res.locals.template)
      res.locals.template = res.locals.model + '/' + res.locals.action;

    // run model context loader if exists
    if (
      we.db.models[res.locals.model] &&
      typeof we.db.models[res.locals.model].contextLoader == 'function'
    ) {
      we.db.models[res.locals.model].contextLoader(req, res, function afterRunModelContextLoader(err) {
        return contextLoaded(err);
      });
    } else {
      return contextLoaded();
    }

    function contextLoaded(err) {
      req.weContextLoaded = true;
      if (err) return next(err);

      hooks.trigger('we:router:request:after:load:context', {
        req: req, res: res
      }, function (err) {
        if (err) return res.serverError(err);
        if (!req.user) return next();
        // preload user roles
        return req.user.getRoles().then(function (result) {
          next(null, result);
        });
      });
    }
  });
}

/**
 * We.js title middleware handlers
 */
router.title = {
  middleware: function titleMiddleware(data, next) {
    if (data.res.locals.titleHandler) {
      if (typeof data.res.locals.titleHandler === 'function') {
        return data.res.locals.titleHandler(data.req, data.res, next);
      } else if (router.title.middlewares[data.res.locals.titleHandler]) {
        return router.title.middlewares[data.res.locals.titleHandler](data.req, data.res, next);
      }
    }

    next();
  },
  middlewares: {
    i18n: function i18n(req, res, next) {
      res.locals.title = req.__(res.locals.titleI18n);
      next();
    },
    recordField: function recordField(req, res, next) {
      if (res.locals.record && res.locals.record) {
        if (res.locals.record.getDataValue) {
          res.locals.title = res.locals.record.get(res.locals.titleField);
        } else {
          res.locals.title = res.locals.record[res.locals.titleField];
        }
      }
      next();
    }
  }
}
hooks.on('we:router:request:after:load:context', router.title.middleware);

/**
 * Add one route after 404 middleware
 */
router.liveBindRoute = function bindRoute(we, route, config) {
  var allRoutes = we.express._router.stack;
  we.router.bindRoute(we, route, config);
  // push this new route to after 404 and error middlewares
  allRoutes.splice(allRoutes.length-3, 0, allRoutes.splice(allRoutes.length-1, 1)[0]);
}

// router.bindShadownRoutes = function bindShadownRoutes(we, cb) {
//   if (!we.db.models || !we.controllers) return cb();

//   var modelNames = Object.keys(we.db.models);

//   var shadownRoutesToBind = [];
//   for (var i = modelNames.length - 1; i >= 0; i--) {
//     if (router.haveShadownRoute(we,  modelNames[i])) {
//       shadownRoutesToBind.push(modelNames[i]);
//     }
//   }

//   shadownRoutesToBind.forEach(function(model) {
//     // get / find
//     router.bindShadownFindRoute(we, model);
//     // post / create
//     router.bindShadownCreateRoute(we, model);
//     // get :id / findOne
//     router.bindShadownFindOneRoute(we, model);
//     // put / update
//     router.bindShadownUpdateRoute(we, model);
//     // delete / destroy
//     router.bindShadownDeleteRoute(we, model);
//   });

//   cb();
// }

/**
 * Check if one model have shadownRoutes
 *
 * @param  {Object} we        We.js object
 * @param  {String} modelName
 * @return {Boolean}
 */
router.haveShadownRoute = function haveShadownRoute(we, modelName) {
  if ( !we.controllers[modelName] || !we.db.models[modelName] ) return false;
  if ( we.controllers[modelName] &&
    we.controllers[modelName]._config &&
    (we.controllers[modelName]._config.shadownRoutes === false)
  ) {
    return false;
  }
  return true;
}

// -- wejs shadown routes bind functions, override if need something diferent

/**
 * Bind find shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownFindRoute = function bindShadownFindRoute(we, model) {
  var configs  = {
    action: 'find',
    model: model,
    controller: model
  };

  var route = '/' + model;

  we.router.bindRoute(we, route, configs);
}

/**
 * Bind create shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownCreateRoute = function bindShadownCreateRoute(we, model) {
  var configs  = {
    method: 'post',
    action: 'create',
    model: model,
    controller: model
  };

  var route = 'post /' + model;

  we.router.bindRoute(we, route, configs);
}

router.bindShadownGenericRoute = function bindShadownGenericRoute(we, model, action, method) {
  var configs  = {
    method: method,
    action: action,
    model: model,
    controller: model
  };

  var route = method + ' /' + model + '/:id';

  we.router.bindRoute(we, route, configs);
}

/**
 * Bind findOne shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownFindOneRoute = function bindShadownFindOneRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'findOne', 'get');
}

/**
 * Bind update shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownUpdateRoute = function bindShadownUpdateRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'update', 'put');
}

/**
 * Bind delete shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownDeleteRoute = function bindShadownDeleteRoute(we, model) {
  router.bindShadownGenericRoute(we, model, 'destroy', 'delete');
}

/**
 * Bind updateAtribute shadown route
 *
 * @param  {object} we      we.js object
 * @param  {string} model   model Name, same as controller name
 */
router.bindShadownUpdateAtributeRoute = function bindShadownUpdateAtributeRoute(we, model) {
  var configs  = {
    method: 'put',
    action: 'updateAtribute',
    model: model,
    controller: model
  };

  var route = 'put /' + model + '/:id/:atribute';
  we.router.bindRoute(we, route, configs);
}

router.singleRecordActions = [
  'findOne', 'update', 'destroy', 'updateAttribute',
  'deleteAttribute', 'addRecord', 'removeRecord', 'getRecord', 'editPage'
];

router.needLoadCurrentModel = function needLoadCurrentModel(req, res) {
  if (res.locals.loadRecord === false) return false;
  if (res.locals.loadRecord === true) return true;

  if (!res.locals.action) return false;

  if ( router.singleRecordActions.indexOf( res.locals.action ) >-1 ) {
    return true;
  }
}

/**
 * Parse query for current request
 */
router.parseQuery = function parseQuery(req, res, next) {
  var we = req.getWe();
  var query = {
    // set subQuery to false by default
    subQuery: false,
    where: {}
  };

  if (res.locals.skipParseQuery) return query;

  if (req.query.offset) {
    query.offset = req.query.offset;
  } else if (req.query.skip) {
    query.offset = req.query.skip;
  }

  if (req.query.limit) {
    query.limit = req.query.limit;
  }

  if (req.query.order) {
    query.order = res.locals.model + '.' +req.query.order;
  } else if (req.query.sort) {
    query.order = res.locals.model + '.' +req.query.sort;
  } else {
    // default order
    query.order = 'createdAt DESC';
  }

  query.include = [{ all: true,  attributes: ['id'] }];

  // parse where
  try {
    if (req.query.where) query.where = JSON.parse( req.query.where );
  } catch(e) {
    we.log.warn('req.query.where have a invalid format', req.query.where);
    return res.badRequest();
  }

  // parse associations
  if (res.locals.model && we.db.models[res.locals.model]) {
    // we.log.info('Models>',we.db.models[res.locals.model]);

    if (we.db.models[res.locals.model].associations) {
      var assocFields = Object.keys(we.db.models[res.locals.model].associations);
      var identifier;
      for (var i = assocFields.length - 1; i >= 0; i--) {
        identifier = we.db.models[res.locals.model].associations[assocFields[i]].identifier;
        // parse in body params
        if (req.body && req.body[assocFields[i]]) {
          req.body[identifier] = req.body[assocFields[i]];
        }
        // parse in where
        if (query.where && query.where[assocFields[i]]) {
          query.where[identifier] = query.where[assocFields[i]];
        }
      }
    }
  }

  return next(null, query);
}

module.exports = router;
<|endoftext|>


<commit_before>import Stack from './stack';
import Queue from './queue';

export class Tile {
    constructor (row, column, x, y, width, height) {
        this.row = row;
        this.column = column;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;

        if (Math.random() < 0.25) {
            this.blocked = true;
        }
        else {
            this.blocked = false;
        }
    }

    toggleBlock () {
        this.blocked = !this.blocked;
    }
}

export class Map {
    constructor (width, height, cellWidth = 10, cellHeight = 10) {
        this.tilesWidthCount = Math.floor(width / cellWidth);
        this.tilesHeightCount = Math.floor(height / cellHeight);

        this.cellWidth = cellWidth;
        this.cellHeight = cellHeight;

        // TODO: sempre deixar um caminho aberto, porém sem força bruta.

        let tries = 10;

        while (tries-- > 0) {
            this.tiles = [];

            for (let i = 0; i < this.tilesHeightCount; i++) {
                for (let j = 0; j < this.tilesWidthCount; j++) {
                    this.tiles.push(new Tile(i, j, j * cellWidth, i * cellHeight, cellWidth, cellHeight));
                }
            }

            if (getPathTo(this, 0, 0, this.tilesHeightCount - 1, this.tilesWidthCount - 1) != null) break;
        }
    }

    getTileCoordinates(x, y) {
        const row = Math.floor(y / this.cellHeight);
        const column = Math.floor(x / this.cellWidth);

        return {
            row,
            column
        };
    }

    getTileFromCoordinates(row, column) {
        return this.tiles.find((tile) => tile.row == row && tile.column == column);
    }

    handleClick({ x, y }) {
        const { row, column } = this.getTileCoordinates(x, y);

        const tile = this.getTileFromCoordinates(row, column);

        if (tile !== undefined) {
            tile.toggleBlock();
        }
    }

    getTotalWidth() {
        return this.tilesWidthCount * this.cellWidth;
    }

    getTotalHeight() {
        return this.tilesHeightCount * this.cellHeight;
    }

    getTiles() {
        return this.tiles;
    }
}

export function getPathTo(map, fromRow, fromColumn, toRow, toColumn) {
    const tiles = [];

    for (let tile of map.getTiles()) {
        tiles[tile.row] = tiles[tile.row] || [];
        tiles[tile.row].push({
            row: tile.row,
            column: tile.column,
            status: tile.blocked,
            mapTile: tile
        });
    }

    if (tiles[fromRow] && tiles[fromRow][fromColumn] && tiles[fromRow][fromColumn].status === false) {
        const queue = new Queue();

        queue.enqueue(tiles[fromRow][fromColumn]);
        tiles[fromRow][fromColumn].status = -1;

        let found = false;

        while (!queue.empty()) {
            const currentNode = queue.dequeue();

            if (currentNode.row == toRow
                && currentNode.column == toColumn) {
                found = true;
                break;
            }

            if (currentNode.row > 0
                && tiles[currentNode.row - 1][currentNode.column].status == false) {
                tiles[currentNode.row - 1][currentNode.column].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row - 1][currentNode.column]);
            }

            if (currentNode.row < map.tilesHeightCount - 1
                && tiles[currentNode.row + 1][currentNode.column].status == false) {
                tiles[currentNode.row + 1][currentNode.column].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row + 1][currentNode.column]);
            }

            if (currentNode.column > 0
                && tiles[currentNode.row][currentNode.column - 1].status == false) {
                tiles[currentNode.row][currentNode.column - 1].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row][currentNode.column - 1]);
            }

            if (currentNode.column < map.tilesWidthCount - 1
                && tiles[currentNode.row][currentNode.column + 1].status == false) {
                tiles[currentNode.row][currentNode.column + 1].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row][currentNode.column + 1]);
            }
        }

        if (found === true) {
            const stack = new Stack();
            const path = [];

            stack.push(tiles[toRow][toColumn]);
            path.push(tiles[toRow][toColumn]);

            while (!stack.empty()) {
                var currentNode = stack.pop();

                path.push(currentNode.mapTile);

                if (currentNode.row > 0
                    && tiles[currentNode.row - 1][currentNode.column].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row - 1][currentNode.column]);
                }
                else if (currentNode.row < map.tilesHeightCount - 1
                    && tiles[currentNode.row + 1][currentNode.column].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row + 1][currentNode.column]);
                }
                else if (currentNode.column > 0
                    && tiles[currentNode.row][currentNode.column - 1].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row][currentNode.column - 1]);
                }
                else if (currentNode.column < map.tilesWidthCount - 1
                    && tiles[currentNode.row][currentNode.column + 1].status == currentNode.status + 1) {
                    queue.enqueue(tiles[currentNode.row][currentNode.column + 1]);
                    stack.push(tiles[currentNode.row][currentNode.column  + 1])
                }
            }

            return path;
        }
    }

    return null;
}
<commit_msg>Refatoração.<commit_after>import Stack from './stack';
import Queue from './queue';

export class Tile {
    constructor (row, column, x, y, width, height) {
        this.row = row;
        this.column = column;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;

        if (Math.random() < 0.25) {
            this.blocked = true;
        }
        else {
            this.blocked = false;
        }
    }

    toggleBlock () {
        this.blocked = !this.blocked;
    }
}

export class Map {
    constructor (width, height, cellWidth = 10, cellHeight = 10) {
        this.tilesWidthCount = Math.floor(width / cellWidth);
        this.tilesHeightCount = Math.floor(height / cellHeight);

        this.cellWidth = cellWidth;
        this.cellHeight = cellHeight;

        // TODO: sempre deixar um caminho aberto, porém sem força bruta.

        let tries = 10;

        while (tries-- > 0) {
            this.tiles = [];

            for (let i = 0; i < this.tilesHeightCount; i++) {
                for (let j = 0; j < this.tilesWidthCount; j++) {
                    this.tiles.push(new Tile(i, j, j * cellWidth, i * cellHeight, cellWidth, cellHeight));
                }
            }

            if (getPathTo(this, 0, 0, this.tilesHeightCount - 1, this.tilesWidthCount - 1) != null) break;
        }
    }

    getTileCoordinates(x, y) {
        const row = Math.floor(y / this.cellHeight);
        const column = Math.floor(x / this.cellWidth);

        return {
            row,
            column
        };
    }

    getTileFromCoordinates(row, column) {
        return this.tiles.find((tile) => tile.row == row && tile.column == column);
    }

    handleClick({ x, y }) {
        const { row, column } = this.getTileCoordinates(x, y);

        const tile = this.getTileFromCoordinates(row, column);

        if (tile !== undefined) {
            tile.toggleBlock();
        }
    }

    getTotalWidth() {
        return this.tilesWidthCount * this.cellWidth;
    }

    getTotalHeight() {
        return this.tilesHeightCount * this.cellHeight;
    }

    getTiles() {
        return this.tiles;
    }
}

export function getPathTo(map, fromRow, fromColumn, toRow, toColumn) {
    const tiles = [];

    for (let tile of map.getTiles()) {
        tiles[tile.row] = tiles[tile.row] || [];
        tiles[tile.row].push({
            row: tile.row,
            column: tile.column,
            status: tile.blocked,
            originalTile: tile
        });
    }

    if (tiles[fromRow] && tiles[fromRow][fromColumn] && tiles[fromRow][fromColumn].status === false) {
        const queue = new Queue();

        queue.enqueue(tiles[fromRow][fromColumn]);
        tiles[fromRow][fromColumn].status = -1;

        let found = false;

        while (!queue.empty()) {
            const currentNode = queue.dequeue();

            if (currentNode.row == toRow
                && currentNode.column == toColumn) {
                found = true;
                break;
            }

            if (currentNode.row > 0
                && tiles[currentNode.row - 1][currentNode.column].status == false) {
                tiles[currentNode.row - 1][currentNode.column].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row - 1][currentNode.column]);
            }

            if (currentNode.row < map.tilesHeightCount - 1
                && tiles[currentNode.row + 1][currentNode.column].status == false) {
                tiles[currentNode.row + 1][currentNode.column].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row + 1][currentNode.column]);
            }

            if (currentNode.column > 0
                && tiles[currentNode.row][currentNode.column - 1].status == false) {
                tiles[currentNode.row][currentNode.column - 1].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row][currentNode.column - 1]);
            }

            if (currentNode.column < map.tilesWidthCount - 1
                && tiles[currentNode.row][currentNode.column + 1].status == false) {
                tiles[currentNode.row][currentNode.column + 1].status = currentNode.status - 1;
                queue.enqueue(tiles[currentNode.row][currentNode.column + 1]);
            }
        }

        if (found === true) {
            const stack = new Stack();
            const path = new Stack();

            stack.push(tiles[toRow][toColumn]);
            path.push(tiles[toRow][toColumn]);

            while (!stack.empty()) {
                var currentNode = stack.pop();

                path.push(currentNode.originalTile);

                if (currentNode.row > 0
                    && tiles[currentNode.row - 1][currentNode.column].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row - 1][currentNode.column]);
                }
                else if (currentNode.row < map.tilesHeightCount - 1
                    && tiles[currentNode.row + 1][currentNode.column].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row + 1][currentNode.column]);
                }
                else if (currentNode.column > 0
                    && tiles[currentNode.row][currentNode.column - 1].status == currentNode.status + 1) {
                    stack.push(tiles[currentNode.row][currentNode.column - 1]);
                }
                else if (currentNode.column < map.tilesWidthCount - 1
                    && tiles[currentNode.row][currentNode.column + 1].status == currentNode.status + 1) {
                    queue.enqueue(tiles[currentNode.row][currentNode.column + 1]);
                    stack.push(tiles[currentNode.row][currentNode.column  + 1])
                }
            }

            const array = [];
            while (!path.empty()) {
                array.push(path.pop());
            }

            return array;
        }
    }

    return null;
}
<|endoftext|>