<commit_before>/*
 * @fileOverview Stealth support.
 */
'use strict';

define(['bitcoinjs-lib', 'convert', 'bigi', 'bs58check', 'buffer'], function(Bitcoin, Convert, BigInteger, base58check, Buffer) {

var bufToArray = function(obj) {return Array.prototype.slice.call(obj, 0);};

var Stealth = {};

// Versions for address format
Stealth.version = 42;
Stealth.testnet = 43;

// Can these be different in the future?
Stealth.nonceVersion = 6;

// Backwards compatibility quirk (0.4.0)
Stealth.quirk = false;

/*
 * Create a bitcoin key with just public component.
 * @param {Object} Q public key as bytes
 * @private
 */
Stealth.importPublic = function(Q) {
    var key = Bitcoin.ECPubKey.fromBytes(Q, true);
    return key;
};

/*
 * Perform curvedh and stealth formatting
 * @param {Bitcoin.BigInteger} e private part
 * @param {Bitcoin.ECKey} decKey public key
 * @private
 */
Stealth.stealthDH = function(e, decKey) {
    // diffie hellman stage
    var point = decKey.Q.multiply(e);

    // start the second stage
    var S1;
    if (Stealth.quirk) {
        S1 = new Buffer([3].concat(point.affineX.toBuffer().toJSON().data));
    } else {
        S1 = point.getEncoded(true);
    }
    var c = Bitcoin.crypto.sha256(S1);
    return BigInteger.fromBuffer(c);
};

/*
 * Format a stealth address in base58
 * @param {Object} scanPubKeyBytes Public key as byte array
 * @param {Object} spendPubKeys Spend public keys as array of byte arrays
 * @param {Number} version Version to use packing the nonce
 *
 * [version:1] [options:1] [scan_pubkey:33] [N:1] [spend_pubkey_1:33] ...
[spend_pubkey_N:33] [number_sigs:1] [prefix_length:1] [prefix:prefix_length/8, round up]
 * version = 255
 * options bitfield = 0 or 1 (reuse scan_pubkey for spends)
 */
Stealth.formatAddress = function(scanPubKeyBytes, spendPubKeys, version) {
    if (version === undefined || version === null) {
        version = Stealth.version;
    }
    if (!spendPubKeys) {
        spendPubKeys = [];
    }
    var reuseScan = spendPubKeys.length ? 0 : 1;

    // Header, version will be added later when encoding
    var stealth = [reuseScan];

    // Add scan public key
    stealth = stealth.concat(scanPubKeyBytes);

    // Add spend public keys
    stealth = stealth.concat([spendPubKeys.length]);
    spendPubKeys.forEach(function(spendPubKey) {
        stealth = stealth.concat(spendPubKey);
    });

    // Number of signatures
    var nSigs = spendPubKeys.length || 1;
    stealth = stealth.concat([nSigs]);

    // TODO: Add prefix
    stealth = stealth.concat([0]);
    // Encode in base58 and add version
    stealth = [version].concat(stealth);
    return base58check.encode(new Buffer(stealth));
};

/*
 * Parse a stealth address into its forming parts
 * @param {String} recipient Address in base58 format
 */
Stealth.parseAddress = function(recipient) {
    // TODO perform consistency checks here
    var stealthBytes = bufToArray(base58check.decode(recipient).slice(1));
    var options = stealthBytes.splice(0, 1)[0];
    var scanKeyBytes = stealthBytes.splice(0, 33);
    var nSpendKeys = stealthBytes.splice(0, 1)[0];

    var spendKeys = [];
    for(var idx=0; idx<nSpendKeys; idx++) {
        spendKeys.push(stealthBytes.splice(0, 33));
    }
    var nSigs = stealthBytes.splice(0, 1)[0];

    // Prefix should be the remaining bytes
    var prefix = stealthBytes.splice(0);

    // Return packed in an object
    return {options: {reuseScan: options},
            scanKey: scanKeyBytes,
            spendKeys: spendKeys,
            sigs: nSigs,
            prefix: prefix};
};

/*
 * Generate a key and related address to send to for a stealth address
 * @param {Object} scanKeyBytes Scanning key as byte array
 * @param {Object} spendKeyBytes Spending key as byte array
 * @param {Number} version Version to use packing the addresses
 * @param {Object} ephemKeyBytes (optional) Ephemeral private key as byte array,
 *                                if null will be generated.
 */
Stealth.initiateStealth = function(scanKeyBytes, spendKeyBytes, version, ephemKeyBytes, newStealth) {
    if (version === null || version === undefined) { version = Bitcoin.networks.bitcoin.pubKeyHash; };
    // Parse public keys into api objects
    var scanKey = Stealth.importPublic(scanKeyBytes);
    var spendKey = Stealth.importPublic(spendKeyBytes);

    // new ephemeral key
    var encKey = Bitcoin.ECKey.fromBytes(ephemKeyBytes);
    var ephemKey = bufToArray(encKey.pub.Q.getEncoded(true));
    if (newStealth) {
        // For new stealth remove the first byte
        ephemKey.splice(0, 1);
    }

    // Generate shared secret
    var c = Stealth.stealthDH(encKey.d, scanKey);

    // Now generate pubkey and address
    var pubKeyBuf = Stealth.derivePublicKey(spendKey, c);

    var mpKeyHash = Bitcoin.crypto.hash160(pubKeyBuf);
    var address = new Bitcoin.Address(mpKeyHash, version);
    return [address, ephemKey, bufToArray(pubKeyBuf)];
};

/*
 * Generate shared secret given the scan secret and an ephemeral key
 * @param {Object} scanSecretBytes Secret as byte array
 * @param {Object} ephemKeyBytes Ephemeral key data as byte array
 * @param {Object} spendKeyBytes Spend key as bytes
 * @private
 */
Stealth.uncoverStealth = function(scanSecret, ephemKeyBytes) {
    // Parse public keys into api objects
    var decKey = Stealth.importPublic(ephemKeyBytes);

    // Parse the secret into a BigInteger
    var priv = BigInteger.fromByteArrayUnsigned(scanSecret.slice(0, 32));

    // Generate shared secret
    return Stealth.stealthDH(priv, decKey);
};

/*
 * Generate a public key bytes for a stealth transaction
 * @param {Object} scanSecretBytes User's scan secret as byte array
 * @param {Object} ephemKeyBytes Tx ephemeral key data as byte array
 * @param {Object} spendKeyBytes User's public spend key as bytes
 * @returns byte array representing the public key
 */
Stealth.uncoverPublic = function(scanSecret, ephemKeyBytes, spendKeyBytes) {
    // Now generate address
    var spendKey = Stealth.importPublic(spendKeyBytes);

    var c = Stealth.uncoverStealth(scanSecret, ephemKeyBytes);

    return Stealth.derivePublicKey(spendKey, c);
};

/*
 * Generate a public key bytes for a stealth transaction
 * @param {Object} scanSecretBytes User's scan secret as byte array
 * @param {Object} ephemKeyBytes Tx ephemeral key data as byte array
 * @param {Object} spendKeyBytes User's private spend key as bytes
 */
Stealth.uncoverPrivate = function(scanSecret, ephemKeyBytes, spendKeyBytes) {
    var c = Stealth.uncoverStealth(scanSecret, ephemKeyBytes);

    // Now generate address
    var spendKey = Bitcoin.ECKey.fromBytes(spendKeyBytes, true);
    return Stealth.derivePrivateKey(spendKey, c);
};

/*
 * Derive a private key from spend key and shared secret
 * @param {Bitcoin.ECKey} spendKey Spend Private Key
 * @param {BigInteger} c Derivation value
 * @returns {Bitcoin.ECKey}
 * @private
 */
Stealth.derivePrivateKey = function(spendKey, c) {
    // Generate the key with the bitcoin api
    return new Bitcoin.ECKey(spendKey.d.add(c).mod(Bitcoin.ECKey.curve.n), spendKey.pub.compressed);
};

/*
 * Derive public key from spendKey and shared secret
 * @param {Bitcoin.ECPubKey} spendKey Spend Key
 * @param {BigInteger} c Derivation value
 * @returns {Bitcoin.ECKey} Derived (compressed) public key bytes
 * @private
 */
Stealth.derivePublicKey = function(spendKey, c) {
    // Now generate address
    var bytes = spendKey.Q
                          .add(new Bitcoin.ECKey(c).pub.Q)
                          .getEncoded(true);

    return bytes;
};

/*
 * Derive a Bitcoin Address from spendKey and shared secret
 * @param {Bitcoin.ECPubKey} spendKey Spend Key
 * @param {BigInteger} c Derivation value
 * @param {Number} version Version to use packing the address
 * @returns {Bitcoin.Address} Derived bitcoin address
 * @private
 */
Stealth.deriveAddress = function(spendKey, c, version) {
    if (version === null || version === undefined) { version = Bitcoin.networks.bitcoin.pubKeyHash; };
    // Now generate address
    var pubKeyBuf = this.derivePublicKey(spendKey, c);

    // Turn to address
    var mpKeyHash = Bitcoin.crypto.hash160(pubKeyBuf);
    var address = new Bitcoin.Address(mpKeyHash, version);
    return address;
};

/*
 * Build the stealth nonce output so it can be added to a transaction.
 * returns a Bitcoin.TransactionOut object.
 * @param {Object} ephemKeyBytes Ephemeral key data as byte array
 * @param {Number} nonce Nonce for the output
 * @param {Number} version Version to use packing the nonce
 * @returns {Bitcoin.TransactionOut} stealth transaction output
 * @private
 */
Stealth.buildNonceOldScript = function(ephemKeyBytes, nonce, version) {
    if (version === null || version === undefined) { version = Stealth.nonceVersion; };

    // Initialize chunks with op_return
    var chunks = [Bitcoin.opcodes.OP_RETURN];

    // Add the nonce chunk
    var nonceBytes = Convert.numToBytes(nonce, 4);
    var ephemScript = [version];
    ephemScript = ephemScript.concat(nonceBytes.concat(ephemKeyBytes));
    chunks.push(new Buffer(ephemScript));

    return Bitcoin.Script.fromChunks(chunks);
};

Stealth.buildNonceNewScript = function(ephemKeyBytes, nonce) {
    // Initialize chunks with op_return
    var chunks = [Bitcoin.opcodes.OP_RETURN];

    // Add the ephemkey chunk
    var nonceBytes = Convert.numToBytes(nonce, 4);

    var ephemScript = ephemKeyBytes.concat(nonceBytes);
    chunks.push(new Buffer(ephemScript));

    return Bitcoin.Script.fromChunks(chunks);
};

Stealth.buildNonceScript = function(ephemKeyBytes, nonce, version) {
    if (version) {
        return this.buildNonceOldScript(ephemKeyBytes, nonce, version);
    } else {
        return this.buildNonceNewScript(ephemKeyBytes, nonce, version);
    }
};

/*
 * Check prefix against the given array
 * returns true or false
 * @param {Object} outHash byte array to compare to
 * @param {Number} prefix prefix array including first byte defining mask
 * @returns {Boolean} whether the prefix matches
 * @private
 */
Stealth.checkPrefix = function(outHash, stealthPrefix) {
    var prefixN = stealthPrefix[0];
    var prefix = stealthPrefix.slice(1);
    var mask = 1<<7;
    var nbyte = 0;
    while(prefixN) {
        // check current bit
        if ((outHash[nbyte] & mask) != (prefix[nbyte] & mask)) {
            return false;
        }
        if (mask === 1) {
            // restart mask and advance byte
            mask = 1<<7;
            nbyte += 1;
        } else {
            // advance mask one bit to the right
            mask = (mask >> 1);
        }
        prefixN -= 1;
    }
    return true;
};

/*
 * Add stealth output to the given transaction and return destination address.
 * returns the new recipient (standard bitcoin address)
 * @param {String} recipient Stealth address in base58 format.
 * @param {Number} addressVersion Version to use packing the addresses
 * @param {Number} nonceVersion Version to use packing the nonce
 * @param {Bitcoin.Transaction} newTx Transaction where we want to add stealth outputs.
 * @param {Object} ephemKeyBytes (optional) Ephemeral private key as byte array,
 *                                if null will be generated.
 */

Stealth.addStealth = function(recipient, newTx, addressVersion, nonceVersion, ephemKeyBytes, initialNonce) {
    if (nonceVersion === undefined) { nonceVersion = Stealth.nonceVersion; };
    if (addressVersion === undefined) { addressVersion = Bitcoin.networks.bitcoin.pubKeyHash; };
    var outHash, ephemKey, pubKey;
    var stealthAddress = Stealth.parseAddress(recipient);
    var stealthPrefix = stealthAddress.prefix;
    var scanKeyBytes = stealthAddress.scanKey;

    // start checking nonce in a random position so we don't leak information
    var maxNonce = Math.pow(2, 32);
    var startingNonce = initialNonce || Math.floor(Math.random()*maxNonce);

    // iterate since we might not find a nonce for our required prefix then
    // we need to create a new ephemkey
    // TODO: Correctly manage spend keys here when there is more than one
    var spendKeyBytes = stealthAddress.spendKeys[0];
    var nonce;
    do {
        var stealthData = Stealth.initiateStealth(scanKeyBytes, spendKeyBytes, addressVersion, ephemKeyBytes, !nonceVersion);
        recipient = stealthData[0];
        ephemKey = stealthData[1];
        pubKey = stealthData[2];
        nonce = startingNonce;
        var iters = 0;
        // iterate through nonces to find a match for given prefix
        do {
            // modify the nonce first so it's unchanged after exiting the loop
            nonce += 1;
            iters += 1;
            if (nonce > maxNonce) {
                nonce = 0;
            }
            var nonceBytes = Convert.numToBytes(nonce, 4);

            // Hash the nonce
            outHash = bufToArray(Bitcoin.crypto.hash160(new Buffer(nonceBytes.concat(ephemKey))));
        } while(iters < maxNonce && !Stealth.checkPrefix(outHash, stealthPrefix));

    } while(!Stealth.checkPrefix(outHash, stealthPrefix));

    // we finally mined the ephemKey that makes the hash match
    var stealthOut = Stealth.buildNonceScript(ephemKey, nonce-1, nonceVersion);
    newTx.addOutput(stealthOut, 0);
    return {address: recipient, ephemKey: ephemKey, pubKey: pubKey};
};

return Stealth;
});
<commit_msg>Properly save the ephemeral when sending.<commit_after>/*
 * @fileOverview Stealth support.
 */
'use strict';

define(['bitcoinjs-lib', 'convert', 'bigi', 'bs58check', 'buffer'], function(Bitcoin, Convert, BigInteger, base58check, Buffer) {

var bufToArray = function(obj) {return Array.prototype.slice.call(obj, 0);};

var Stealth = {};

// Versions for address format
Stealth.version = 42;
Stealth.testnet = 43;

// Can these be different in the future?
Stealth.nonceVersion = 6;

// Backwards compatibility quirk (0.4.0)
Stealth.quirk = false;

/*
 * Create a bitcoin key with just public component.
 * @param {Object} Q public key as bytes
 * @private
 */
Stealth.importPublic = function(Q) {
    var key = Bitcoin.ECPubKey.fromBytes(Q, true);
    return key;
};

/*
 * Perform curvedh and stealth formatting
 * @param {Bitcoin.BigInteger} e private part
 * @param {Bitcoin.ECKey} decKey public key
 * @private
 */
Stealth.stealthDH = function(e, decKey) {
    // diffie hellman stage
    var point = decKey.Q.multiply(e);

    // start the second stage
    var S1;
    if (Stealth.quirk) {
        S1 = new Buffer([3].concat(point.affineX.toBuffer().toJSON().data));
    } else {
        S1 = point.getEncoded(true);
    }
    var c = Bitcoin.crypto.sha256(S1);
    return BigInteger.fromBuffer(c);
};

/*
 * Format a stealth address in base58
 * @param {Object} scanPubKeyBytes Public key as byte array
 * @param {Object} spendPubKeys Spend public keys as array of byte arrays
 * @param {Number} version Version to use packing the nonce
 *
 * [version:1] [options:1] [scan_pubkey:33] [N:1] [spend_pubkey_1:33] ...
[spend_pubkey_N:33] [number_sigs:1] [prefix_length:1] [prefix:prefix_length/8, round up]
 * version = 255
 * options bitfield = 0 or 1 (reuse scan_pubkey for spends)
 */
Stealth.formatAddress = function(scanPubKeyBytes, spendPubKeys, version) {
    if (version === undefined || version === null) {
        version = Stealth.version;
    }
    if (!spendPubKeys) {
        spendPubKeys = [];
    }
    var reuseScan = spendPubKeys.length ? 0 : 1;

    // Header, version will be added later when encoding
    var stealth = [reuseScan];

    // Add scan public key
    stealth = stealth.concat(scanPubKeyBytes);

    // Add spend public keys
    stealth = stealth.concat([spendPubKeys.length]);
    spendPubKeys.forEach(function(spendPubKey) {
        stealth = stealth.concat(spendPubKey);
    });

    // Number of signatures
    var nSigs = spendPubKeys.length || 1;
    stealth = stealth.concat([nSigs]);

    // TODO: Add prefix
    stealth = stealth.concat([0]);
    // Encode in base58 and add version
    stealth = [version].concat(stealth);
    return base58check.encode(new Buffer(stealth));
};

/*
 * Parse a stealth address into its forming parts
 * @param {String} recipient Address in base58 format
 */
Stealth.parseAddress = function(recipient) {
    // TODO perform consistency checks here
    var stealthBytes = bufToArray(base58check.decode(recipient).slice(1));
    var options = stealthBytes.splice(0, 1)[0];
    var scanKeyBytes = stealthBytes.splice(0, 33);
    var nSpendKeys = stealthBytes.splice(0, 1)[0];

    var spendKeys = [];
    for(var idx=0; idx<nSpendKeys; idx++) {
        spendKeys.push(stealthBytes.splice(0, 33));
    }
    var nSigs = stealthBytes.splice(0, 1)[0];

    // Prefix should be the remaining bytes
    var prefix = stealthBytes.splice(0);

    // Return packed in an object
    return {options: {reuseScan: options},
            scanKey: scanKeyBytes,
            spendKeys: spendKeys,
            sigs: nSigs,
            prefix: prefix};
};

/*
 * Generate a key and related address to send to for a stealth address
 * @param {Object} scanKeyBytes Scanning key as byte array
 * @param {Object} spendKeyBytes Spending key as byte array
 * @param {Number} version Version to use packing the addresses
 * @param {Object} ephemKeyBytes (optional) Ephemeral private key as byte array,
 *                                if null will be generated.
 */
Stealth.initiateStealth = function(scanKeyBytes, spendKeyBytes, version, ephemKeyBytes, newStealth) {
    if (version === null || version === undefined) { version = Bitcoin.networks.bitcoin.pubKeyHash; };
    // Parse public keys into api objects
    var scanKey = Stealth.importPublic(scanKeyBytes);
    var spendKey = Stealth.importPublic(spendKeyBytes);

    // new ephemeral key
    var encKey = Bitcoin.ECKey.fromBytes(ephemKeyBytes);
    var ephemKey = bufToArray(encKey.pub.Q.getEncoded(true));
    // For new stealth we will later remove the first byte of the ephem

    // Generate shared secret
    var c = Stealth.stealthDH(encKey.d, scanKey);

    // Now generate pubkey and address
    var pubKeyBuf = Stealth.derivePublicKey(spendKey, c);

    var mpKeyHash = Bitcoin.crypto.hash160(pubKeyBuf);
    var address = new Bitcoin.Address(mpKeyHash, version);
    return [address, ephemKey, bufToArray(pubKeyBuf)];
};

/*
 * Generate shared secret given the scan secret and an ephemeral key
 * @param {Object} scanSecretBytes Secret as byte array
 * @param {Object} ephemKeyBytes Ephemeral key data as byte array
 * @param {Object} spendKeyBytes Spend key as bytes
 * @private
 */
Stealth.uncoverStealth = function(scanSecret, ephemKeyBytes) {
    // Parse public keys into api objects
    var decKey = Stealth.importPublic(ephemKeyBytes);

    // Parse the secret into a BigInteger
    var priv = BigInteger.fromByteArrayUnsigned(scanSecret.slice(0, 32));

    // Generate shared secret
    return Stealth.stealthDH(priv, decKey);
};

/*
 * Generate a public key bytes for a stealth transaction
 * @param {Object} scanSecretBytes User's scan secret as byte array
 * @param {Object} ephemKeyBytes Tx ephemeral key data as byte array
 * @param {Object} spendKeyBytes User's public spend key as bytes
 * @returns byte array representing the public key
 */
Stealth.uncoverPublic = function(scanSecret, ephemKeyBytes, spendKeyBytes) {
    // Now generate address
    var spendKey = Stealth.importPublic(spendKeyBytes);

    var c = Stealth.uncoverStealth(scanSecret, ephemKeyBytes);

    return Stealth.derivePublicKey(spendKey, c);
};

/*
 * Generate a public key bytes for a stealth transaction
 * @param {Object} scanSecretBytes User's scan secret as byte array
 * @param {Object} ephemKeyBytes Tx ephemeral key data as byte array
 * @param {Object} spendKeyBytes User's private spend key as bytes
 */
Stealth.uncoverPrivate = function(scanSecret, ephemKeyBytes, spendKeyBytes) {
    var c = Stealth.uncoverStealth(scanSecret, ephemKeyBytes);

    // Now generate address
    var spendKey = Bitcoin.ECKey.fromBytes(spendKeyBytes, true);
    return Stealth.derivePrivateKey(spendKey, c);
};

/*
 * Derive a private key from spend key and shared secret
 * @param {Bitcoin.ECKey} spendKey Spend Private Key
 * @param {BigInteger} c Derivation value
 * @returns {Bitcoin.ECKey}
 * @private
 */
Stealth.derivePrivateKey = function(spendKey, c) {
    // Generate the key with the bitcoin api
    return new Bitcoin.ECKey(spendKey.d.add(c).mod(Bitcoin.ECKey.curve.n), spendKey.pub.compressed);
};

/*
 * Derive public key from spendKey and shared secret
 * @param {Bitcoin.ECPubKey} spendKey Spend Key
 * @param {BigInteger} c Derivation value
 * @returns {Bitcoin.ECKey} Derived (compressed) public key bytes
 * @private
 */
Stealth.derivePublicKey = function(spendKey, c) {
    // Now generate address
    var bytes = spendKey.Q
                          .add(new Bitcoin.ECKey(c).pub.Q)
                          .getEncoded(true);

    return bytes;
};

/*
 * Derive a Bitcoin Address from spendKey and shared secret
 * @param {Bitcoin.ECPubKey} spendKey Spend Key
 * @param {BigInteger} c Derivation value
 * @param {Number} version Version to use packing the address
 * @returns {Bitcoin.Address} Derived bitcoin address
 * @private
 */
Stealth.deriveAddress = function(spendKey, c, version) {
    if (version === null || version === undefined) { version = Bitcoin.networks.bitcoin.pubKeyHash; };
    // Now generate address
    var pubKeyBuf = this.derivePublicKey(spendKey, c);

    // Turn to address
    var mpKeyHash = Bitcoin.crypto.hash160(pubKeyBuf);
    var address = new Bitcoin.Address(mpKeyHash, version);
    return address;
};

/*
 * Build the stealth nonce output so it can be added to a transaction.
 * returns a Bitcoin.TransactionOut object.
 * @param {Object} ephemKeyBytes Ephemeral key data as byte array
 * @param {Number} nonce Nonce for the output
 * @param {Number} version Version to use packing the nonce
 * @returns {Bitcoin.TransactionOut} stealth transaction output
 * @private
 */
Stealth.buildNonceOldScript = function(ephemKeyBytes, nonce, version) {
    if (version === null || version === undefined) { version = Stealth.nonceVersion; };

    // Initialize chunks with op_return
    var chunks = [Bitcoin.opcodes.OP_RETURN];

    // Add the nonce chunk
    var nonceBytes = Convert.numToBytes(nonce, 4);
    var ephemScript = [version];
    ephemScript = ephemScript.concat(nonceBytes.concat(ephemKeyBytes));
    chunks.push(new Buffer(ephemScript));

    return Bitcoin.Script.fromChunks(chunks);
};

Stealth.buildNonceNewScript = function(ephemKeyBytes, nonce) {
    // Initialize chunks with op_return
    var chunks = [Bitcoin.opcodes.OP_RETURN];

    // Add the ephemkey chunk
    var nonceBytes = Convert.numToBytes(nonce, 4);

    var ephemScript = ephemKeyBytes.concat(nonceBytes);
    chunks.push(new Buffer(ephemScript));

    return Bitcoin.Script.fromChunks(chunks);
};

Stealth.buildNonceScript = function(ephemKeyBytes, nonce, version) {
    if (version) {
        return this.buildNonceOldScript(ephemKeyBytes, nonce, version);
    } else {
        return this.buildNonceNewScript(ephemKeyBytes, nonce, version);
    }
};

/*
 * Check prefix against the given array
 * returns true or false
 * @param {Object} outHash byte array to compare to
 * @param {Number} prefix prefix array including first byte defining mask
 * @returns {Boolean} whether the prefix matches
 * @private
 */
Stealth.checkPrefix = function(outHash, stealthPrefix) {
    var prefixN = stealthPrefix[0];
    var prefix = stealthPrefix.slice(1);
    var mask = 1<<7;
    var nbyte = 0;
    while(prefixN) {
        // check current bit
        if ((outHash[nbyte] & mask) != (prefix[nbyte] & mask)) {
            return false;
        }
        if (mask === 1) {
            // restart mask and advance byte
            mask = 1<<7;
            nbyte += 1;
        } else {
            // advance mask one bit to the right
            mask = (mask >> 1);
        }
        prefixN -= 1;
    }
    return true;
};

/*
 * Add stealth output to the given transaction and return destination address.
 * returns the new recipient (standard bitcoin address)
 * @param {String} recipient Stealth address in base58 format.
 * @param {Number} addressVersion Version to use packing the addresses
 * @param {Number} nonceVersion Version to use packing the nonce
 * @param {Bitcoin.Transaction} newTx Transaction where we want to add stealth outputs.
 * @param {Object} ephemKeyBytes (optional) Ephemeral private key as byte array,
 *                                if null will be generated.
 */

Stealth.addStealth = function(recipient, newTx, addressVersion, nonceVersion, ephemKeyBytes, initialNonce) {
    if (nonceVersion === undefined) { nonceVersion = Stealth.nonceVersion; };
    if (addressVersion === undefined) { addressVersion = Bitcoin.networks.bitcoin.pubKeyHash; };
    var outHash, ephemKey, pubKey, ephemKeyRaw;
    var stealthAddress = Stealth.parseAddress(recipient);
    var stealthPrefix = stealthAddress.prefix;
    var scanKeyBytes = stealthAddress.scanKey;

    // start checking nonce in a random position so we don't leak information
    var maxNonce = Math.pow(2, 32);
    var startingNonce = initialNonce || Math.floor(Math.random()*maxNonce);

    // iterate since we might not find a nonce for our required prefix then
    // we need to create a new ephemkey
    // TODO: Correctly manage spend keys here when there is more than one
    var spendKeyBytes = stealthAddress.spendKeys[0];
    var nonce;
    do {
        var stealthData = Stealth.initiateStealth(scanKeyBytes, spendKeyBytes, addressVersion, ephemKeyBytes, !nonceVersion);
        recipient = stealthData[0];
        ephemKeyRaw = stealthData[1];
        ephemKey = ephemKeyRaw.slice(0);
        if (!nonceVersion) {
            ephemKey.splice(0, 1);
        }
        pubKey = stealthData[2];
        nonce = startingNonce;
        var iters = 0;
        // iterate through nonces to find a match for given prefix
        do {
            // modify the nonce first so it's unchanged after exiting the loop
            nonce += 1;
            iters += 1;
            if (nonce > maxNonce) {
                nonce = 0;
            }
            var nonceBytes = Convert.numToBytes(nonce, 4);

            // Hash the nonce
            outHash = bufToArray(Bitcoin.crypto.hash160(new Buffer(nonceBytes.concat(ephemKey))));
        } while(iters < maxNonce && !Stealth.checkPrefix(outHash, stealthPrefix));

    } while(!Stealth.checkPrefix(outHash, stealthPrefix));

    // we finally mined the ephemKey that makes the hash match
    var stealthOut = Stealth.buildNonceScript(ephemKey, nonce-1, nonceVersion);
    newTx.addOutput(stealthOut, 0);
    return {address: recipient, ephemKey: ephemKeyRaw, pubKey: pubKey};
};

return Stealth;
});
<|endoftext|>


<commit_before>// parse.js
// Reads an schema and retrieves the proper options from it

// Errors specifics to this submodule
const OptionsError = require('./errors');
const path = require('path');

// The main function.
// - arg: user options from the argument in the main server() function
// - env: the environment variables as read from env.js
// - parent: if it's a submodule, the global configuration
module.exports = async (schema, arg = {}, env= {}, parent = {}) => {

  // Fully parsed options will be stored here
  const options = {};

  // For plugins, accept "false" as an option to nuke a full plugin
  if (arg === false && parent) {
    return false;
  }

  // Accepts a single option instead of an object and it will be mapped to its
  // root value. Example: server(2000) === server({ port: 2000 })
  if (typeof arg !== 'object') {
    if (!schema.__root) {
      throw new OptionsError('/server/options/notobject');
    }
    arg = { [schema.__root]: arg };
  }

  // Loop each of the defined options
  for (let name in schema) {

    // RETRIEVAL
    // Make the definition local so it's easier to handle
    const def = schema[name];
    let value;

    // Skip the control variables such as '__root'
    if (/^__/.test(name)) continue;

    // Make sure we are dealing with a valid schema definition for this option
    if (typeof def !== 'object') {
      throw new Error('Invalid option definition: ' + JSON.stringify(def));
    }

    // The user defined a function to find the actual value manually
    if (def.find) {
      value = await def.find({ arg, env, def, parent, schema });
    } else {

      // Use the user-passed option unles explictly told not to
      if (def.arg !== false) {
        def.arg = def.arg === true ? name : def.arg || name;
      } else if (arg[name] && process.env.NODE_ENV === 'test') {
        throw new OptionsError('/server/options/noarg', { name });
      }

      // Use the environment variable unless explicitly told not to
      if (def.env !== false) {
        def.env = (def.env === true ? name : def.env || name).toUpperCase();
      } else if (env[name.toUpperCase()] && process.env.NODE_ENV === 'test') {
        if (!/^win/.test(process.platform) || name !== 'public') {
          throw new OptionsError('/server/options/noenv', { name });
        }
      }

      // List of possibilities, from HIGHER preference to LOWER preference
      // Removes the empty one and gets the first one as it has HIGHER preference
      const possible = [
        env[def.env],
        arg[def.arg],
        parent[def.inherit],
        def.default
      ].filter(value => typeof value !== 'undefined');
      if (possible.length) {
        value = possible[0];
      }
    }

    // Extend the base object or user object with new values if these are not set
    if (def.extend && (typeof value === 'undefined' || typeof value === 'object')) {
      if (typeof value === 'undefined') {
        value = {};
      }
      value = Object.assign({}, def.default, value);
    }

    // Normalize the "public" folder or file
    if ((def.file || def.folder) && typeof value === 'string') {
      if (!path.isAbsolute(value)) {
        value = path.join(process.cwd(), value);
      }
      value = path.normalize(value);
    }

    // A final hook for the schema to call up on the value
    if (def.clean) {
      value = await def.clean(value, { arg, env, parent, def, schema });
    }

    // VALIDATION
    // Validate that it is set
    if (def.required) {
      if (typeof value === 'undefined') {
        throw new OptionsError('/server/options/required', { name });
      }
      // TODO: check that the file and folder exist
    }

    if (def.enum) {
      if (!def.enum.includes(value)) {
        throw new OptionsError('/server/options/enum', { name, value, possible: def.enum });
      }
    }

    // Validate the type (only if there's a value)
    if (def.type && value) {

      // Parse valid types into a simple array of strings: ['string', 'number']
      def.type = (def.type instanceof Array ? def.type : [def.type])
        // pulls up the name for primitives such as String, Number, etc
        .map(one => (one.name ? one.name : one).toLowerCase());

      // Make sure it is one of the valid types
      if (!def.type.includes(typeof value)) {
        throw new OptionsError('/server/options/type', {
          name, expected: def.type, value
        });
      }
    }

    if (def.validate) {
      let ret = def.validate(value, def, options);
      if (ret instanceof Error) throw ret;
      if (!ret) throw new OptionsError('/server/options/validate', { name, value });
    }

    if (typeof value !== 'undefined') {
      options[name] = value;
    }
  }

  return options;
};
<commit_msg>Windows debugging<commit_after>// parse.js
// Reads an schema and retrieves the proper options from it

// Errors specifics to this submodule
const OptionsError = require('./errors');
const path = require('path');

// The main function.
// - arg: user options from the argument in the main server() function
// - env: the environment variables as read from env.js
// - parent: if it's a submodule, the global configuration
module.exports = async (schema, arg = {}, env= {}, parent = {}) => {

  // Fully parsed options will be stored here
  const options = {};

  // For plugins, accept "false" as an option to nuke a full plugin
  if (arg === false && parent) {
    return false;
  }

  // Accepts a single option instead of an object and it will be mapped to its
  // root value. Example: server(2000) === server({ port: 2000 })
  if (typeof arg !== 'object') {
    if (!schema.__root) {
      throw new OptionsError('/server/options/notobject');
    }
    arg = { [schema.__root]: arg };
  }

  // Loop each of the defined options
  for (let name in schema) {

    // RETRIEVAL
    // Make the definition local so it's easier to handle
    const def = schema[name];
    let value;

    // Skip the control variables such as '__root'
    if (/^__/.test(name)) continue;

    // Make sure we are dealing with a valid schema definition for this option
    if (typeof def !== 'object') {
      throw new Error('Invalid option definition: ' + JSON.stringify(def));
    }

    // The user defined a function to find the actual value manually
    if (def.find) {
      value = await def.find({ arg, env, def, parent, schema });
    } else {

      // Use the user-passed option unles explictly told not to
      if (def.arg !== false) {
        def.arg = def.arg === true ? name : def.arg || name;
      } else if (arg[name] && process.env.NODE_ENV === 'test') {
        throw new OptionsError('/server/options/noarg', { name });
      }

      // Use the environment variable unless explicitly told not to
      if (def.env !== false) {
        def.env = (def.env === true ? name : def.env || name).toUpperCase();
      } else if (env[name.toUpperCase()] && process.env.NODE_ENV === 'test') {
        if (!/^win/.test(process.platform) || name !== 'public') {
          throw new OptionsError('/server/options/noenv', { name });
        }
      }

      // List of possibilities, from HIGHER preference to LOWER preference
      // Removes the empty one and gets the first one as it has HIGHER preference
      const possible = [
        env[def.env],
        arg[def.arg],
        parent[def.inherit],
        def.default
      ].filter(value => typeof value !== 'undefined');
      if (possible.length) {
        value = possible[0];
      }
    }

    // Extend the base object or user object with new values if these are not set
    if (def.extend && (typeof value === 'undefined' || typeof value === 'object')) {
      if (typeof value === 'undefined') {
        value = {};
      }
      value = Object.assign({}, def.default, value);
    }

    // Normalize the "public" folder or file
    if ((def.file || def.folder) && typeof value === 'string') {
      if (!path.isAbsolute(value)) {
        value = path.join(process.cwd(), value);
      }
      value = path.normalize(value);
    }

    // A final hook for the schema to call up on the value
    if (def.clean) {
      value = await def.clean(value, { arg, env, parent, def, schema });
    }

    // VALIDATION
    // Validate that it is set
    if (def.required) {
      if (typeof value === 'undefined') {
        throw new OptionsError('/server/options/required', { name });
      }
      // TODO: check that the file and folder exist
    }

    if (def.enum) {
      if (!def.enum.includes(value)) {
        throw new OptionsError('/server/options/enum', { name, value, possible: def.enum });
      }
    }

    // Validate the type (only if there's a value)
    if (def.type && value) {

      // Parse valid types into a simple array of strings: ['string', 'number']
      def.type = (def.type instanceof Array ? def.type : [def.type])
        // pulls up the name for primitives such as String, Number, etc
        .map(one => (one.name ? one.name : one).toLowerCase());

      // Make sure it is one of the valid types
      if (!def.type.includes(typeof value)) {
        throw new OptionsError('/server/options/type', {
          name, expected: def.type, value
        });
      }
    }

    if (def.validate) {
      let ret = def.validate(value, def, options);
      if (ret instanceof Error) throw ret;
      if (!ret) throw new OptionsError('/server/options/validate', { name, value });
    }

    options[name] = value;
    // }
  }

  return options;
};
<|endoftext|>


<commit_before>openFarmApp.directive('formChecker', function(){
  return {
    // scope: {
    //   'formId': '=',
    //   'formStage': '='
    // },
    require: '^form',
    link: function(scope, element, attr){
      // loop through each stage
      var rootParent = scope.$parent.$parent.$parent;

      scope.$watch('$parent.stage', function(){
        var allDone = [];
        rootParent.stages.forEach(function(stage){
          if (stage.selected){
            stage.where.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            stage.light.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            stage.soil.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            // Point the next step button to the next
            // stage.

            allDone.push(stage.edited ? true : false);
          }
        });
        var tracker = true;
        allDone.forEach(function(isDone){
          if (!isDone){
            tracker = false;
          }
        });

        rootParent.stageThreeTracker = tracker;
      }, true);

    }
  };
});

openFarmApp.directive('stageButtons', [
  function stageButtons(){
    return {
      restrict: 'A',
      scope: {
          abledBool: '&',
          nextFunction: '=',
          processing: '='
      },
      controller: ['$scope', '$element', '$attrs',
       function ($scope, $element, $attrs){
        // Takes in attributes and set them to the appropriate
        // variable on the local scope.
        $scope.$watch('processing', function(){
          if ($scope.processing === true){
            $scope.disabledText = 'This may take some time';
          }
        });
        $scope.abledText = $attrs.abledText || 'Continue';
        $scope.disabledText =
          $attrs.disabledText || 'You can\'t continue yet.';
        $scope.cancelText = $attrs.cancelText || 'Cancel.';
        $scope.cancelUrl = $attrs.cancelUrl || '/';
        $scope.backText = $attrs.backText || undefined;

        $scope.previousStep = $scope.$parent.previousStep;
        $scope.nextStep = $scope.nextFunction || $scope.$parent.nextStep;
      }],
      templateUrl: '/assets/templates/_stage_buttons.html'
    };
}]);

// ToDo: move things to the angular.openfarm services in guides.
openFarmApp.controller('newGuideCtrl', ['$scope', '$http', '$filter',
  'guideService', 'stageService',
  function newGuideCtrl($scope, $http, $filter, guideService, stageService) {
  $scope.alerts = [];
  $scope.crops = [];
  $scope.step = 1;
  $scope.crop_not_found = false;
  $scope.addresses = [];
  $scope.stages = [];
  $scope.hasEdited = [];

  // What's a new guide.
  $scope.newGuide = {
    name: '',
    crop: undefined,
    overview: '',
    selectedStages: [],
    exists: false,
    stages: [],
    practices: [
      {slug: 'organic', label: 'Organic', selected: false},
      {slug: 'permaculture', label: 'Permaculture', selected: false},
      {slug: 'conventional', label: 'Conventional', selected: false},
      {slug: 'hydroponic', label: 'Hydroponic', selected: false},
      {slug: 'intensive', label: 'Intensive', selected: false}
    ]
  };

  $scope.$watch('newGuide.stages', function(){
    $scope.newGuide.selectedStages = [];
    var stages = $scope.newGuide.stages;
    if (stages){
      var lastSelectedIndex = null;
      stages.forEach(function(item, index){
        item.nextSelectedIndex = undefined;
        if (item.selected){
          item.originalIndex = index;
          if (lastSelectedIndex !== null){
            item.lastSelectedIndex = lastSelectedIndex;
            stages[lastSelectedIndex].nextSelectedIndex = index;
          }

          $scope.newGuide.selectedStages.push(item);
          lastSelectedIndex = index;
        }
      });
    }
    $scope.newGuide.selectedStages.sort(function(a, b){
      return a.order > b.order;
    });
  }, true);

  $scope.$watch('step', function(afterValue){
    if (afterValue === 3){
      var selectedSet = false;
      $scope.newGuide.selectedStages.forEach(function(stage){
        if (stage.selected && !selectedSet){
          // hacked hack is a hack
          $scope.newGuide.stages[stage.originalIndex].editing = true;
          selectedSet = true;
        } else {
          $scope.newGuide.stages[stage.originalIndex].editing = false;
        }
      });
    }
  });

  var getStages = function(){
    $http.get('/api/stage_options/')
      .success(function(response){
        var stageWhere = ['Potted', 'Outside', 'Greenhouse', 'Indoors'];
        var stageLight = ['Full Sun', 'Partial Sun', 'Shaded', 'Darkness'];
        var stageSoil = ['Potting', 'Loam',
                         'Sandy Loam', 'Clay Loam',
                         'Sand', 'Clay'];
        $scope.stages = response.stage_options;
        $scope.stages = $filter('orderBy')($scope.stages, 'order');
        // Trickery to make sure the existing stages don't get
        // overwritten
        $scope.stages.forEach(function(item){
          item.selected = false;

          // loop over the existing stages.
          $scope.newGuide.stages.forEach(function(d){
            if (d.name === item.name){
              // And copy over the relevant stuff.
              item.selected = true;
              item.exists = true;
              item._id = d._id;

              item.stage_length = d.stage_length;
              switch(true){
                case (parseInt(d.stage_length, 10) % 7 === 0):
                  item.stage_length = item.stage_length / 7;
                  item.length_type = 'weeks';
                  break;
                case (parseInt(d.stage_length, 10) % 30 === 0):
                  item.stage_length = item.stage_length / 30;
                  item.length_type = 'months';
                  break;
                default:
                  item.length_type = 'days';
              }

              item.where = $scope.buildStageDetails(stageWhere,
                                                    d.environment || []);
              item.light = $scope.buildStageDetails(stageLight,
                                                    d.light || []);
              item.soil = $scope.buildStageDetails(stageSoil,
                                                   d.soil || []);
            }
          });
          // TODO: The below probably needs to be broken out
          // and made *way* more dynamic.
          if (!item.where){
            item.where = $scope.buildStageDetails(stageWhere, []);
          }
          if (!item.light){
            item.light = $scope.buildStageDetails(stageLight, []);
          }
          if (!item.soil){
            item.soil = $scope.buildStageDetails(stageSoil, []);
          }
          return item;
        });

        $scope.newGuide.stages = $scope.stages;
      })
      .error(function(response, code){
        $scope.alerts.push({
          msg: code + ' error. We had trouble fetching all stage options.',
          type: 'warning'
        });
      });
  };

  if (getIDFromURL('guides') && getIDFromURL('guides') !== 'new'){
    $http.get('/api/guides/' + getIDFromURL('guides'))
      .success(function(r){
        $scope.newGuide.exists = true;
        $scope.newGuide._id = r.guide._id;
        $scope.newGuide.featured_image = r.guide.featured_image;
        $scope.s3upload = r.guide.featured_image;
        $scope.newGuide.name = r.guide.name;

        if (r.guide.practices){
          $scope.newGuide.practices.forEach(function(d){
            if (r.guide.practices.indexOf(d.slug) !== -1){
              d.selected = true;
            }
          });
        }

        if (r.guide.stages){
          r.guide.stages.forEach(function(d){
            d.exists = true;
            $scope.newGuide.stages.push(d);
          });
        }

        getStages();

        processCropID(r.guide.crop_id);
      })
      .error(function(r, e){
        $scope.alerts.push({
          msg: e,
          type: 'alert'
        });
        console.log(r, e);
      });
  } else {
    getStages();
  }

  var processCropID = function(crop_id) {
    if (crop_id){
      $http.get('/api/crops/' + crop_id)
        .success(function(r){
          $scope.newGuide.crop = r.crop;
          $scope.query = r.crop.name;
        })
        .error(function(r, e){
          $scope.alerts.push({
            msg: e,
            type: 'alert'
          });
          console.log(r, e);
        });
    }
  };

  processCropID(getUrlVar('crop_id'));

  //Typeahead search for crops
  $scope.search = function () {
    // be nice and only hit the server if
    // length >= 3
    if ($scope.query.length >= 3){
      $http({
        url: '/api/crops',
        method: "GET",
        params: {
          query: $scope.query
        }
      }).success(function (response) {
        if (response.crops.length){
          $scope.crops = response.crops;
        } else {
          $scope.crop_not_found = true;
        }
      }).error(function (response, code) {
        $scope.alerts.push({
          msg: code + ' error. Could not retrieve data from server. Please try again later.',
          type: 'warning'
        });
      });
    }
  };

  //Gets fired when user selects dropdown.
  $scope.cropSelected = function ($item, $model, $label) {
    $scope.newGuide.crop = $item;
    $scope.crop_not_found = false;
    $scope.newGuide.crop.description = '';
  };

  $scope.createCrop = function(){
    window.location.href = '/crops/new/?name=' + $scope.query;
  };

  $scope.nextStep = function(){
    $scope.step += 1;
  };

  $scope.previousStep = function(){
    $scope.step -= 1;
  };

  $scope.nextStage = function(index){
    $scope.editSelectedStage($scope.stages[index]);
  };

  $scope.editSelectedStage = function(stage){
    $scope.newGuide.selectedStages.forEach(function(item){
      item.editing = false;
      if (stage === item){
        item.editing = true;
      }
    });
  };

  $scope.buildStageDetails = function(array, selectedArray){
    var returnArray = [];
    array.forEach(function(d){
      var obj = {
        slug: d.toLowerCase().replace(/ /g,'_').replace(/[^\w-]+/g,''),
        label: d,
        selected: selectedArray.indexOf(d) === -1 ? false : true,
      };
      returnArray.push(obj);
    });
    return returnArray;
  };

  // The submit process.
  // Get the practices and clean them up.
  // Set up the parameters.
  // Post! & forward if successful

  $scope.submitForm = function () {
    $scope.newGuide.sending = true;
    var practices = [];
    angular.forEach($scope.newGuide.practices, function(value, key){
      if (value.selected){
        practices.push(value.slug);
      }
    }, practices);

    var params = {
      name: $scope.newGuide.name,
      crop_id: $scope.newGuide.crop._id,
      overview: $scope.newGuide.overview || null,
      location: $scope.newGuide.location || null,
      featured_image: $scope.newGuide.featured_image || null,
      practices: practices
    };
    if (params.featured_image === '/assets/leaf-grey.png'){
      params.featured_image = null;
    }
    if ($scope.newGuide._id){
      // In this case the guide already existed,
      // so we need to put, not to post.
      params._id = $scope.newGuide._id;
      guideService.updateGuide(params._id,
                               params,
                               $scope.alerts,
                               $scope.sendStages);
    } else {
      guideService.createGuide(params,
                               $scope.alerts,
                               $scope.sendStages);
    }
  };

  $scope.sendStages = function(success, guide){
    $scope.newGuide._id = guide._id;
    $scope.sent = 0;

    var calcStageLength = function(length, length_type){
      if (length && length_type){
        switch (length_type){
          case 'months':
          return length * 30;
          case 'weeks':
          return length * 7;
          default:
          return length;
        }
      } else {
        return null;
      }
    };

    $scope.newGuide.stages.forEach(function(stage){
      var stageParams = {
        name: stage.name,
        images: [stage.featured_image],
        guide_id: guide._id,
        stage_length: calcStageLength(stage.stage_length, stage.length_type),
        environment: stage.where.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null,
        soil: stage.soil.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null,
        light: stage.light.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null
      };

      // Go through all the possible changes on
      // each stage.
      if (stage.selected && !stage.exists){
        stageService.createStage(stageParams,
                                 $scope.alerts,
                                 function(success, stage){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });

      } else if (stage.selected && stage.exists){
        stageService.updateStage(stage._id,
                                 stageParams,
                                 $scope.alerts,
                                 function(){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });

      } else if (stage.exists){
        stageService.deleteStage(stage._id,
                                 $scope.alerts,
                                 function(){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });
      }
    });
  };

  // Only redirect when everything is done processing.
  $scope.checkNumberUpdated = function(){
    var updatedNum = 0;
    $scope.newGuide.stages.forEach(function(stage){
      if (stage.selected || stage.exists){
        updatedNum++;
      }
    });
    if (updatedNum === $scope.sent){
      window.location.href = '/guides/' + $scope.newGuide._id + '/';
    }
  };

  // Any function returning a promise object can be used to load
  // values asynchronously

  $scope.cancel = function(path){
    window.location.href = path || '/';
  };
}]);
<commit_msg>Oh right, Hound doesn't update<commit_after>openFarmApp.directive('formChecker', function(){
  return {
    // scope: {
    //   'formId': '=',
    //   'formStage': '='
    // },
    require: '^form',
    link: function(scope, element, attr){
      // loop through each stage
      var rootParent = scope.$parent.$parent.$parent;

      scope.$watch('$parent.stage', function(){
        var allDone = [];
        rootParent.stages.forEach(function(stage){
          if (stage.selected){
            stage.where.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            stage.light.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            stage.soil.forEach(function(opt){
              if (opt.selected){
                stage.edited = true;
              }
            });
            // Point the next step button to the next
            // stage.

            allDone.push(stage.edited ? true : false);
          }
        });
        var tracker = true;
        allDone.forEach(function(isDone){
          if (!isDone){
            tracker = false;
          }
        });

        rootParent.stageThreeTracker = tracker;
      }, true);

    }
  };
});

openFarmApp.directive('stageButtons', [
  function stageButtons(){
    return {
      restrict: 'A',
      scope: {
          abledBool: '&',
          nextFunction: '=',
          processing: '='
      },
      controller: ['$scope', '$element', '$attrs',
       function ($scope, $element, $attrs){
        // Takes in attributes and set them to the appropriate
        // variable on the local scope.
        $scope.$watch('processing', function(){
          if ($scope.processing === true){
            $scope.disabledText = 'This may take some time';
          }
        });
        $scope.abledText = $attrs.abledText || 'Continue';
        $scope.disabledText =
          $attrs.disabledText || 'You can\'t continue yet.';
        $scope.cancelText = $attrs.cancelText || 'Cancel.';
        $scope.cancelUrl = $attrs.cancelUrl || '/';
        $scope.backText = $attrs.backText || undefined;

        $scope.previousStep = $scope.$parent.previousStep;
        $scope.nextStep = $scope.nextFunction || $scope.$parent.nextStep;
      }],
      templateUrl: '/assets/templates/_stage_buttons.html'
    };
}]);

// ToDo: move things to the angular.openfarm services in guides.
openFarmApp.controller('newGuideCtrl', ['$scope', '$http', '$filter',
  'guideService', 'stageService',
  function newGuideCtrl($scope, $http, $filter, guideService, stageService) {
  $scope.alerts = [];
  $scope.crops = [];
  $scope.step = 1;
  $scope.crop_not_found = false;
  $scope.addresses = [];
  $scope.stages = [];
  $scope.hasEdited = [];

  // What's a new guide.
  $scope.newGuide = {
    name: '',
    crop: undefined,
    overview: '',
    selectedStages: [],
    exists: false,
    stages: [],
    practices: [
      {slug: 'organic', label: 'Organic', selected: false},
      {slug: 'permaculture', label: 'Permaculture', selected: false},
      {slug: 'conventional', label: 'Conventional', selected: false},
      {slug: 'hydroponic', label: 'Hydroponic', selected: false},
      {slug: 'intensive', label: 'Intensive', selected: false}
    ]
  };

  $scope.$watch('newGuide.stages', function(){
    $scope.newGuide.selectedStages = [];
    var stages = $scope.newGuide.stages;
    if (stages){
      var lastSelectedIndex = null;
      stages.forEach(function(item, index){
        item.nextSelectedIndex = undefined;
        if (item.selected){
          item.originalIndex = index;
          if (lastSelectedIndex !== null){
            item.lastSelectedIndex = lastSelectedIndex;
            stages[lastSelectedIndex].nextSelectedIndex = index;
          }

          $scope.newGuide.selectedStages.push(item);
          lastSelectedIndex = index;
        }
      });
    }
    $scope.newGuide.selectedStages.sort(function(a, b){
      return a.order > b.order;
    });
  }, true);

  $scope.$watch('step', function(afterValue){
    if (afterValue === 3){
      var selectedSet = false;
      $scope.newGuide.selectedStages.forEach(function(stage){
        if (stage.selected && !selectedSet){
          // hacked hack is a hack
          $scope.newGuide.stages[stage.originalIndex].editing = true;
          selectedSet = true;
        } else {
          $scope.newGuide.stages[stage.originalIndex].editing = false;
        }
      });
    }
  });

  var getStages = function(){
    $http.get('/api/stage_options/')
      .success(function(response){
        var stageWhere = ['Potted', 'Outside', 'Greenhouse', 'Indoors'];
        var stageLight = ['Full Sun', 'Partial Sun', 'Shaded', 'Darkness'];
        var stageSoil = ['Potting', 'Loam',
                         'Sandy Loam', 'Clay Loam',
                         'Sand', 'Clay'];
        $scope.stages = response.stage_options;
        $scope.stages = $filter('orderBy')($scope.stages, 'order');
        // Trickery to make sure the existing stages don't get
        // overwritten
        $scope.stages.forEach(function(item){
          item.selected = false;

          // loop over the existing stages.
          $scope.newGuide.stages.forEach(function(d){
            if (d.name === item.name){
              // And copy over the relevant stuff.
              item.selected = true;
              item.exists = true;
              item._id = d._id;

              item.stage_length = d.stage_length;
              switch(true){
                case (parseInt(d.stage_length, 10) % 7 === 0):
                  item.stage_length = item.stage_length / 7;
                  item.length_type = 'weeks';
                  break;
                case (parseInt(d.stage_length, 10) % 30 === 0):
                  item.stage_length = item.stage_length / 30;
                  item.length_type = 'months';
                  break;
                default:
                  item.length_type = 'days';
              }

              item.where = $scope.buildStageDetails(stageWhere,
                                                    d.environment || []);
              item.light = $scope.buildStageDetails(stageLight,
                                                    d.light || []);
              item.soil = $scope.buildStageDetails(stageSoil,
                                                   d.soil || []);
            }
          });
          // TODO: The below probably needs to be broken out
          // and made *way* more dynamic.
          if (!item.where){
            item.where = $scope.buildStageDetails(stageWhere, []);
          }
          if (!item.light){
            item.light = $scope.buildStageDetails(stageLight, []);
          }
          if (!item.soil){
            item.soil = $scope.buildStageDetails(stageSoil, []);
          }
          return item;
        });

        $scope.newGuide.stages = $scope.stages;
      })
      .error(function(response, code){
        $scope.alerts.push({
          msg: code + ' error. We had trouble fetching all stage options.',
          type: 'warning'
        });
      });
  };

  if (getIDFromURL('guides') && getIDFromURL('guides') !== 'new'){
    $http.get('/api/guides/' + getIDFromURL('guides'))
      .success(function(r){
        $scope.newGuide.exists = true;
        $scope.newGuide._id = r.guide._id;
        $scope.newGuide.featured_image = r.guide.featured_image;
        $scope.s3upload = r.guide.featured_image;
        $scope.newGuide.name = r.guide.name;

        if (r.guide.practices){
          $scope.newGuide.practices.forEach(function(d){
            if (r.guide.practices.indexOf(d.slug) !== -1){
              d.selected = true;
            }
          });
        }

        if (r.guide.stages){
          r.guide.stages.forEach(function(d){
            d.exists = true;
            $scope.newGuide.stages.push(d);
          });
        }

        getStages();

        processCropID(r.guide.crop_id);
      })
      .error(function(r, e){
        $scope.alerts.push({
          msg: e,
          type: 'alert'
        });
        console.log(r, e);
      });
  } else {
    getStages();
  }

  var processCropID = function(crop_id) {
    if (crop_id){
      $http.get('/api/crops/' + crop_id)
        .success(function(r){
          $scope.newGuide.crop = r.crop;
          $scope.query = r.crop.name;
        })
        .error(function(r, e){
          $scope.alerts.push({
            msg: e,
            type: 'alert'
          });
          console.log(r, e);
        });
    }
  };

  processCropID(getUrlVar('crop_id'));

  //Typeahead search for crops
  $scope.search = function () {
    // be nice and only hit the server if
    // length >= 3
    if ($scope.query.length >= 3){
      $http({
        url: '/api/crops',
        method: "GET",
        params: {
          query: $scope.query
        }
      }).success(function (response) {
        if (response.crops.length){
          $scope.crops = response.crops;
        } else {
          $scope.crop_not_found = true;
        }
      }).error(function (response, code) {
        $scope.alerts.push({
          msg: code + ' error. Could not retrieve data from server. Please try again later.',
          type: 'warning'
        });
      });
    }
  };

  //Gets fired when user selects dropdown.
  $scope.cropSelected = function ($item, $model, $label) {
    $scope.newGuide.crop = $item;
    $scope.crop_not_found = false;
    $scope.newGuide.crop.description = '';
  };

  $scope.createCrop = function(){
    window.location.href = '/crops/new/?name=' + $scope.query;
  };

  $scope.nextStep = function(){
    $scope.step += 1;
  };

  $scope.previousStep = function(){
    $scope.step -= 1;
  };

  $scope.nextStage = function(index){
    $scope.editSelectedStage($scope.stages[index]);
  };

  $scope.editSelectedStage = function(stage){
    $scope.newGuide.selectedStages.forEach(function(item){
      item.editing = false;
      if (stage === item){
        item.editing = true;
      }
    });
  };

  $scope.buildStageDetails = function(array, selectedArray){
    var returnArray = [];
    array.forEach(function(d){
      var obj = {
        slug: d.toLowerCase().replace(/ /g,'_').replace(/[^\w-]+/g,''),
        label: d,
        selected: selectedArray.indexOf(d) === -1 ? false : true,
      };
      returnArray.push(obj);
    });
    return returnArray;
  };

  // The submit process.
  // Get the practices and clean them up.
  // Set up the parameters.
  // Post! & forward if successful

  $scope.submitForm = function () {
    $scope.newGuide.sending = true;
    var practices = [];
    angular.forEach($scope.newGuide.practices, function(value, key){
      if (value.selected){
        practices.push(value.slug);
      }
    }, practices);

    var params = {
      name: $scope.newGuide.name,
      crop_id: $scope.newGuide.crop._id,
      overview: $scope.newGuide.overview || null,
      location: $scope.newGuide.location || null,
      featured_image: $scope.newGuide.featured_image || null,
      practices: practices
    };
    if (params.featured_image === '/assets/leaf-grey.png'){
      params.featured_image = null;
    }
    if ($scope.newGuide._id){
      // In this case the guide already existed,
      // so we need to put, not to post.
      params._id = $scope.newGuide._id;
      guideService.updateGuide(params._id,
                               params,
                               $scope.alerts,
                               $scope.sendStages);
    } else {
      guideService.createGuide(params,
                               $scope.alerts,
                               $scope.sendStages);
    }
  };

  var calcStageLength = function(length, length_type){
    if (length && length_type){
      switch (length_type){
        case 'months':
        return length * 30;
        case 'weeks':
        return length * 7;
        default:
        return length;
      }
    } else {
      return null;
    }
  };

  $scope.sendStages = function(success, guide){
    $scope.newGuide._id = guide._id;
    $scope.sent = 0;

    $scope.newGuide.stages.forEach(function(stage){
      var stageParams = {
        name: stage.name,
        images: [stage.featured_image],
        guide_id: guide._id,
        stage_length: calcStageLength(stage.stage_length, stage.length_type),
        environment: stage.where.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null,
        soil: stage.soil.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null,
        light: stage.light.filter(function(s){
            return s.selected;
          }).map(function(s){
            return s.label;
          }) || null
      };

      // Go through all the possible changes on
      // each stage.
      if (stage.selected && !stage.exists){
        stageService.createStage(stageParams,
                                 $scope.alerts,
                                 function(success, stage){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });

      } else if (stage.selected && stage.exists){
        stageService.updateStage(stage._id,
                                 stageParams,
                                 $scope.alerts,
                                 function(){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });

      } else if (stage.exists){
        stageService.deleteStage(stage._id,
                                 $scope.alerts,
                                 function(){
                                   stage.sent = true;
                                   $scope.sent ++;
                                   $scope.checkNumberUpdated();
                                 });
      }
    });
  };

  // Only redirect when everything is done processing.
  $scope.checkNumberUpdated = function(){
    var updatedNum = 0;
    $scope.newGuide.stages.forEach(function(stage){
      if (stage.selected || stage.exists){
        updatedNum++;
      }
    });
    if (updatedNum === $scope.sent){
      window.location.href = '/guides/' + $scope.newGuide._id + '/';
    }
  };

  // Any function returning a promise object can be used to load
  // values asynchronously

  $scope.cancel = function(path){
    window.location.href = path || '/';
  };
}]);
<|endoftext|>


<commit_before>/**
 * The router module.
 *
 * Router handles app routing and URL parameters and updates Presenter.
 *
 * @return singleton instance of Router class (extends Backbone.Router).
 */
define([
  'underscore',
  'backbone',
  'amplify',
  'map/utils',
  'map/services/PlaceService'
], function(_, Backbone, amplify, utils, PlaceService) {

  'use strict';

  var Router = Backbone.Router.extend({

    // temporary, we will do it with env variables
    _cacheVersion: 48,

    routes: {
      'map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'map',
      'embed/map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'embed'
    },

    /**
     * Boot file:
     *
     * @param  {[type]} boot [description]
     */
    initialize: function(mainView) {
      this.bind('all', this._checkForCacheBust());
      this.name = null;
      this.mainView = mainView;
      this.placeService = new PlaceService(this);
    },

    map: function() {
      this.name = 'map';
      this.initMap.apply(this, arguments);
    },

    embed: function() {
      this.name = 'embed/map';
      this.initMap.apply(this, arguments);
    },

    initMap: function(zoom, lat, lng, iso, maptype, baselayers, sublayers, subscribe, referral) {
      var params = _.extend({
        zoom: zoom,
        lat: lat,
        lng: lng,
        iso: iso,
        maptype: maptype,
        baselayers: baselayers,
        sublayers: sublayers,
        subscribe_alerts: subscribe,
        referral: referral
      }, _.parseUrl());
      this.placeService.initPlace(this.name, params);
    },

    /**
     * If the URL contains the cache parameter (e.g., cache=bust), clear all
     * cached values in the browser (e.g., from memory, local storage,
     * session).
     */
    _checkForCacheBust: function() {
      var params = _.parseUrl();
      var localCacheVersion = _.toNumber(
        localStorage.getItem('CACHE_VERSION'));

      if (_.has(params, 'cache') || localCacheVersion !== this._cacheVersion) {
        _.each(amplify.store(), function(value, key) {
          amplify.store(key, null);
        });

        localStorage.setItem('CACHE_VERSION', this._cacheVersion);
      }
    },

    navigateTo: function(route, options) {
      this.navigate(route, options);
    }

  });

  return Router;

});
<commit_msg>Router: cache version<commit_after>/**
 * The router module.
 *
 * Router handles app routing and URL parameters and updates Presenter.
 *
 * @return singleton instance of Router class (extends Backbone.Router).
 */
define([
  'underscore',
  'backbone',
  'amplify',
  'map/utils',
  'map/services/PlaceService'
], function(_, Backbone, amplify, utils, PlaceService) {

  'use strict';

  var Router = Backbone.Router.extend({

    // temporary, we will do it with env variables
    _cacheVersion: 49,

    routes: {
      'map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'map',
      'embed/map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'embed'
    },

    /**
     * Boot file:
     *
     * @param  {[type]} boot [description]
     */
    initialize: function(mainView) {
      this.bind('all', this._checkForCacheBust());
      this.name = null;
      this.mainView = mainView;
      this.placeService = new PlaceService(this);
    },

    map: function() {
      this.name = 'map';
      this.initMap.apply(this, arguments);
    },

    embed: function() {
      this.name = 'embed/map';
      this.initMap.apply(this, arguments);
    },

    initMap: function(zoom, lat, lng, iso, maptype, baselayers, sublayers, subscribe, referral) {
      var params = _.extend({
        zoom: zoom,
        lat: lat,
        lng: lng,
        iso: iso,
        maptype: maptype,
        baselayers: baselayers,
        sublayers: sublayers,
        subscribe_alerts: subscribe,
        referral: referral
      }, _.parseUrl());
      this.placeService.initPlace(this.name, params);
    },

    /**
     * If the URL contains the cache parameter (e.g., cache=bust), clear all
     * cached values in the browser (e.g., from memory, local storage,
     * session).
     */
    _checkForCacheBust: function() {
      var params = _.parseUrl();
      var localCacheVersion = _.toNumber(
        localStorage.getItem('CACHE_VERSION'));

      if (_.has(params, 'cache') || localCacheVersion !== this._cacheVersion) {
        _.each(amplify.store(), function(value, key) {
          amplify.store(key, null);
        });

        localStorage.setItem('CACHE_VERSION', this._cacheVersion);
      }
    },

    navigateTo: function(route, options) {
      this.navigate(route, options);
    }

  });

  return Router;

});
<|endoftext|>


<commit_before>/**
 * The router module.
 *
 * Router handles app routing and URL parameters and updates Presenter.
 *
 * @return singleton instance of Router class (extends Backbone.Router).
 */
define([
  'underscore',
  'backbone',
  'amplify',
  'map/utils',
  'map/services/PlaceService'
], function(_, Backbone, amplify, utils, PlaceService) {

  'use strict';

  var Router = Backbone.Router.extend({

    // temporary, we will do it with env variables
    _cacheVersion: 53,

    routes: {
      'map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'map',
      'embed/map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'embed'
    },

    /**
     * Boot file:
     *
     * @param  {[type]} boot [description]
     */
    initialize: function(mainView) {
      this.isLocalStorageNameSupported();
      this.bind('all', this._checkForCacheBust());
      this.name = null;
      this.mainView = mainView;
      this.placeService = new PlaceService(this);

    },

    map: function() {
      this.name = 'map';
      this.initMap.apply(this, arguments);
    },

    embed: function() {
      this.name = 'embed/map';
      this.initMap.apply(this, arguments);
    },

    initMap: function(zoom, lat, lng, iso, maptype, baselayers, sublayers, subscribe, referral) {
      var params = _.extend({
        zoom: zoom,
        lat: lat,
        lng: lng,
        iso: iso,
        maptype: maptype,
        baselayers: baselayers,
        sublayers: sublayers,
        subscribe_alerts: subscribe,
        referral: referral
      }, _.parseUrl());
      this.placeService.initPlace(this.name, params);
    },

    /**
     * If the URL contains the cache parameter (e.g., cache=bust), clear all
     * cached values in the browser (e.g., from memory, local storage,
     * session).
     */
    _checkForCacheBust: function() {
      var params = _.parseUrl();
      var localCacheVersion = _.toNumber(
        localStorage.getItem('CACHE_VERSION'));

      if (_.has(params, 'cache') || localCacheVersion !== this._cacheVersion) {
        _.each(amplify.store(), function(value, key) {
          amplify.store(key, null);
        });

        localStorage.setItem('CACHE_VERSION', this._cacheVersion);
      }
    },

    isLocalStorageNameSupported: function() {
      // Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem
      // throw QuotaExceededError. We're going to detect this and just silently drop any calls to setItem
      // to avoid the entire page breaking, without having to do a check at each usage of Storage.
      if (typeof localStorage === 'object') {
        try {
          localStorage.setItem('localStorage', 1);
          localStorage.removeItem('localStorage');
        } catch (e) {
          Storage.prototype._setItem = Storage.prototype.setItem;
          Storage.prototype.setItem = function() {};
          alert('You may not be able to experience full functionality of the map while in private mode because your web browser does not support storing settings locally. In Safari, the most common cause of this is using "Private Browsing Mode".');
        }
      }
    },

    navigateTo: function(route, options) {
      this.navigate(route, options);
    }

  });

  return Router;

});
<commit_msg>cache version ++<commit_after>/**
 * The router module.
 *
 * Router handles app routing and URL parameters and updates Presenter.
 *
 * @return singleton instance of Router class (extends Backbone.Router).
 */
define([
  'underscore',
  'backbone',
  'amplify',
  'map/utils',
  'map/services/PlaceService'
], function(_, Backbone, amplify, utils, PlaceService) {

  'use strict';

  var Router = Backbone.Router.extend({

    // temporary, we will do it with env variables
    _cacheVersion: 54,

    routes: {
      'map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'map',
      'embed/map(/:zoom)(/:lat)(/:lng)(/:iso)(/:maptype)(/:baselayers)(/:sublayers)(/)': 'embed'
    },

    /**
     * Boot file:
     *
     * @param  {[type]} boot [description]
     */
    initialize: function(mainView) {
      this.isLocalStorageNameSupported();
      this.bind('all', this._checkForCacheBust());
      this.name = null;
      this.mainView = mainView;
      this.placeService = new PlaceService(this);

    },

    map: function() {
      this.name = 'map';
      this.initMap.apply(this, arguments);
    },

    embed: function() {
      this.name = 'embed/map';
      this.initMap.apply(this, arguments);
    },

    initMap: function(zoom, lat, lng, iso, maptype, baselayers, sublayers, subscribe, referral) {
      var params = _.extend({
        zoom: zoom,
        lat: lat,
        lng: lng,
        iso: iso,
        maptype: maptype,
        baselayers: baselayers,
        sublayers: sublayers,
        subscribe_alerts: subscribe,
        referral: referral
      }, _.parseUrl());
      this.placeService.initPlace(this.name, params);
    },

    /**
     * If the URL contains the cache parameter (e.g., cache=bust), clear all
     * cached values in the browser (e.g., from memory, local storage,
     * session).
     */
    _checkForCacheBust: function() {
      var params = _.parseUrl();
      var localCacheVersion = _.toNumber(
        localStorage.getItem('CACHE_VERSION'));

      if (_.has(params, 'cache') || localCacheVersion !== this._cacheVersion) {
        _.each(amplify.store(), function(value, key) {
          amplify.store(key, null);
        });

        localStorage.setItem('CACHE_VERSION', this._cacheVersion);
      }
    },

    isLocalStorageNameSupported: function() {
      // Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem
      // throw QuotaExceededError. We're going to detect this and just silently drop any calls to setItem
      // to avoid the entire page breaking, without having to do a check at each usage of Storage.
      if (typeof localStorage === 'object') {
        try {
          localStorage.setItem('localStorage', 1);
          localStorage.removeItem('localStorage');
        } catch (e) {
          Storage.prototype._setItem = Storage.prototype.setItem;
          Storage.prototype.setItem = function() {};
          alert('You may not be able to experience full functionality of the map while in private mode because your web browser does not support storing settings locally. In Safari, the most common cause of this is using "Private Browsing Mode".');
        }
      }
    },

    navigateTo: function(route, options) {
      this.navigate(route, options);
    }

  });

  return Router;

});
<|endoftext|>