<commit_before>/**
 * ***** BEGIN LICENSE BLOCK *****
 * OpenPGP Zimbra Secure is the open source digital signature and encrypt for Zimbra Collaboration Open Source Edition software
 * Copyright (C) 2016-present Nguyen Van Nguyen <nguyennv1981@gmail.com>

 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 * ***** END LICENSE BLOCK *****
 *
 * OpenPGP MIME Secure Email Zimlet
 *
 * Written by Nguyen Van Nguyen <nguyennv1981@gmail.com>
 */

/**
 * Key store constructor.
 */
OpenPGPSecureKeyStore = function(handler) {
    this._handler = handler;
    this._fingerprints = [];
    this._addCallbacks = [];
    this._removeCallbacks = [];

    var uid = this._handler.getUserID();
    this._privateKeyItem = 'openpgp-secure-private-key-' + uid;
    this._passphraseItem = 'openpgp-secure-passphrase-' + uid;
    this._publicKeysItem = 'openpgp-secure-public-keys-' + uid;

    this.passphrase = '';
    this.privateKey = false;
    this.publicKeys = [];
    this.contactPublicKeys = [];
    this.globalPublicKeys = [];
};

OpenPGPSecureKeyStore.ADD_CALLBACK = 'add';
OpenPGPSecureKeyStore.REMOVE_CALLBACK = 'remove';

OpenPGPSecureKeyStore.prototype = new Object();
OpenPGPSecureKeyStore.prototype.constructor = OpenPGPSecureKeyStore;

/**
 * Initialize key store.
 */
OpenPGPSecureKeyStore.prototype.init = function() {
    var self = this;

    var sequence = Promise.resolve();
    return sequence.then(function() {
        return self.setPublicKeys(self._readPublicKeys());
    })
    .then(function() {
        self._scanContacts();
        self._globalTrust();
        return;
    })
    .then(function() {
        return OpenPGPUtils.localStorageRead(
            self._passphraseItem,
            self._handler.getSecurePassword()
        )
        .then(function(passphrase) {
            return passphrase;
        });
    })
    .then(function(passphrase) {
        if (passphrase) {
            return OpenPGPUtils.localStorageRead(
                self._privateKeyItem,
                self._handler.getSecurePassword()
            )
            .then(function(privateKey) {
                if (privateKey) {
                    var privKey = openpgp.key.readArmored(privateKey);
                    privKey.keys.forEach(function(key) {
                        if (key.decrypt(passphrase)) {
                            self.privateKey = key;
                            self.passphrase = passphrase;
                        }
                        else {
                            throw new Error(self._handler.getMessage('decryptPrivateKeyError'));
                        }
                    });
                }
                return self.privateKey;
            });
        }
        else {
            return self.privateKey;
        }
    })
    .then(function(privateKey) {
        if (privateKey) {
            self.addPublicKey(privateKey.toPublic());
        }
    });
};

/**
 * Add public key to key store.
 *
 * @param {Key} key OpenPGP public key
 */
OpenPGPSecureKeyStore.prototype.addPublicKey = function(key) {
    if (key.isPublic()) {
        var self = this;
        var added = false;

        var fingerprint = key.primaryKey.getFingerprint();
        if (!self._fingerprints[fingerprint]) {
            self._fingerprints[fingerprint] = fingerprint;
            self.publicKeys.push(key);
            added = true;
        }

        if (added) {
            this._addCallbacks.forEach(function(callback) {
                callback.run(key);
            });
            this._storePublicKeys();
        }
    }
};

/**
 * Remove public key from key store.
 *
 * @param {String} fingerprint OpenPGP key fingerprint
 */
OpenPGPSecureKeyStore.prototype.removePublicKey = function(fingerprint) {
    var self = this;
    var removed = false;
    this.publicKeys.forEach(function(key, index) {
        if (fingerprint == key.primaryKey.getFingerprint()) {
            self.publicKeys.splice(index, 1);
            delete self._fingerprints[fingerprint];
            removed = true;
        }
    });

    if (removed) {
        this._removeCallbacks.forEach(function(callback) {
            callback.run(fingerprint);
        });
        this._storePublicKeys();
    }
};

/**
 * Add callback to key store.
 *
 * @param {AjxCallback} callback
 * @param {String} type Callback type [add or remove]
 */
OpenPGPSecureKeyStore.prototype.addCallback = function(callback, type) {
    if (type === OpenPGPSecureKeyStore.REMOVE_CALLBACK) {
        this._removeCallbacks.push(callback);
    }
    else {
        this._addCallbacks.push(callback);
    }
};

/**
 * Get passphrase of private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPassphrase = function() {
    return this.passphrase;
};

/**
 * Get private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPrivateKey = function() {
    return this.privateKey;
};

/**
 * Set private key to key store.
 *
 * @param {String} privateKey Armored private key
 * @param {String} passphrase Passphrase for key decrypting
 */
OpenPGPSecureKeyStore.prototype.setPrivateKey = function(privateKey, passphrase) {
    var self = this;
    var privKey = openpgp.key.readArmored(privateKey);
    privKey.keys.forEach(function(key) {
        if (key.decrypt(passphrase)) {
            self.privateKey = key;
            self.passphrase = passphrase;
            self.addPublicKey(key.toPublic());

            OpenPGPUtils.localStorageSave(
                self._privateKeyItem,
                self._handler.getSecurePassword(),
                privateKey
            );
            OpenPGPUtils.localStorageSave(
                self._passphraseItem,
                self._handler.getSecurePassword(),
                passphrase
            );
        }
        else {
            throw new Error(this._handler.getMessage('decryptPrivateKeyError'));
        }
    });
    return this;
};

/**
 * Get public key from private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPublicKey = function() {
    return this.privateKey ? this.privateKey.toPublic() : false;
};

/**
 * Get public keys from store.
 */
OpenPGPSecureKeyStore.prototype.getPublicKeys = function() {
    return this.publicKeys;
};

/**
 * Get all public keys.
 */
OpenPGPSecureKeyStore.prototype.getAllPublicKeys = function() {
    var publicKeys = this.publicKeys;
    return publicKeys.concat(this.contactPublicKeys).concat(this.globalPublicKeys);
};

/**
 * Set all public keys to key store.
 *
 * @param {String} armoredKeys An armored public keys
 */
OpenPGPSecureKeyStore.prototype.setPublicKeys = function(armoredKeys) {
    if (armoredKeys) {
        var self = this;
        this.publicKeys.splice(0);
        var pubKeys = openpgp.key.readArmored(armoredKeys);
        pubKeys.keys.forEach(function(key) {
            var fingerprint = key.primaryKey.getFingerprint();
            if (key.isPublic() && !self._fingerprints[fingerprint]) {
                self.publicKeys.push(key);
                self._fingerprints[fingerprint] = fingerprint;
            }
        });
    }
    return this;
};

/**
 * Get public keys matching with email addresss.
 *
 * @param {Array} addresses Email addresses to search for
 */
OpenPGPSecureKeyStore.prototype.havingPublicKeys = function(addresses) {
    var having = [];
    var dupes = [];
    var publicKeys = this.getAllPublicKeys();
    publicKeys.forEach(function(key) {
        var userIds = key.getUserIds();
        addresses.forEach(function(address) {
            userIds.forEach(function(uid) {
                var fingerprint = key.primaryKey.getFingerprint();
                if (uid.indexOf(address) >= 0 && !dupes[fingerprint + uid]) {
                    having.push(key);
                    dupes[fingerprint + uid] = fingerprint + uid;
                }
            });
        });
    });
    return having;
};

/**
 * Get email addesses missing public key.
 *
 * @param {Array} addresses Email addresses to search for
 */
OpenPGPSecureKeyStore.prototype.publicKeyMissing = function(addresses) {
    var uidAddresses = [];
    var publicKeys = this.getAllPublicKeys();
    publicKeys.forEach(function(key) {
        var userIds = key.getUserIds();
        userIds.forEach(function(uid) {
            var addr = AjxEmailAddress.parse(uid);
            if (addr) {
                uidAddresses[addr.getAddress()] = addr.getAddress();
            }
        });
    });

    var missing = [];
    addresses.forEach(function(address) {
        if (!uidAddresses[address]) {
            missing.push(address);
        }
    });
    return missing;
};

/**
 * Check public key is existed in key store.
 *
 * @param {String} fingerprint Fingerprint string of the key
 */
OpenPGPSecureKeyStore.prototype.publicKeyExisted = function(fingerprint) {
    return this._fingerprints[fingerprint] ? true : false;
};

/**
 * Export all public keys in store.
 */
OpenPGPSecureKeyStore.prototype.exportPublicKeys = function() {
    var packetlist = new openpgp.packet.List();
    this.publicKeys.forEach(function(key) {
        packetlist.read(key.toPacketlist().write());
    });
    return openpgp.armor.encode(openpgp.enums.armor.public_key, packetlist.write());
};

/**
 * Export all public keys.
 */
OpenPGPSecureKeyStore.prototype.exportAllPublicKeys = function() {
    var publicKeys = this.getAllPublicKeys();
    var packetlist = new openpgp.packet.List();
    publicKeys.forEach(function(key) {
        packetlist.read(key.toPacketlist().write());
    });
    return openpgp.armor.encode(openpgp.enums.armor.public_key, packetlist.write());
};

/**
 * Genarate public key information.
 */
OpenPGPSecureKeyStore.keyInfo = function(key) {
    var uids = [];
    var userIds = key.getUserIds();
    if (userIds) {
        userIds.forEach(function(uid) {
            uids.push(AjxStringUtil.htmlEncode(uid));
        });
    }

    var priKey = key.primaryKey;
    var keyLength = '';
    if (priKey.mpi.length > 0) {
        keyLength = (priKey.mpi[0].byteLength() * 8);
    }
    return {
        value: key.armor(),
        uids: uids,
        fingerprint: priKey.getFingerprint(),
        keyid: priKey.getKeyId().toHex(),
        algorithm: priKey.algorithm,
        keyLength: keyLength,
        created: priKey.created
    };
};

/**
 * Read all public keys from storage.
 */
OpenPGPSecureKeyStore.prototype._readPublicKeys = function() {
    return localStorage[this._publicKeysItem];
};

/**
 * Store all public keys to storage.
 */
OpenPGPSecureKeyStore.prototype._storePublicKeys = function() {
    localStorage[this._publicKeysItem] = this.exportPublicKeys();
    appCtxt.notifyZimlet(OpenPGPZimbraSecure.NAME, 'onPublicKeyChange');
};

/**
 * Scan public keys from user contacts.
 */
OpenPGPSecureKeyStore.prototype._scanContacts = function() {
    var url = [
        OpenPGPUtils.restUrl(), '/contacts?fmt=csv'
    ].join('');

    var callback = new AjxCallback(function(response) {
        if (response.success) {
            var self = this;
            var entries = response.text.split('"');
            entries.forEach(function(entry) {
                if (OpenPGPUtils.hasInlinePGPContent(entry, OpenPGPUtils.OPENPGP_PUBLIC_KEY_HEADER)) {
                    var contactKeys = openpgp.key.readArmored(entry);
                    contactKeys.keys.forEach(function(key) {
                        var fingerprint = key.primaryKey.getFingerprint();
                        if (key.isPublic() && !self._fingerprints[fingerprint]) {
                            self.contactPublicKeys.push(key);
                            self._fingerprints[fingerprint] = fingerprint;
                        }
                    });
                }
            });
        }
    });

    AjxRpc.invoke('', url, {}, callback, true);
};

/**
 * Load public keys from global trust.
 */
OpenPGPSecureKeyStore.prototype._globalTrust = function() {
    var resource = this._handler.getZimletContext().getConfig('global-trust');
    if (resource) {
        var self = this;
        var url = this._handler.getResource(resource);
        var callback = new AjxCallback(function(response) {
            if (response.success) {
                var globalKeys = openpgp.key.readArmored(response.text);
                globalKeys.keys.forEach(function(key) {
                    var fingerprint = key.primaryKey.getFingerprint();
                    if (key.isPublic() && !self._fingerprints[fingerprint]) {
                        self.globalPublicKeys.push(key);
                        self._fingerprints[fingerprint] = fingerprint;
                    }
                });
            }
        });

        AjxRpc.invoke('', url, {}, callback, true);
    }
};
<commit_msg>Fix typo<commit_after>/**
 * ***** BEGIN LICENSE BLOCK *****
 * OpenPGP Zimbra Secure is the open source digital signature and encrypt for Zimbra Collaboration Open Source Edition software
 * Copyright (C) 2016-present Nguyen Van Nguyen <nguyennv1981@gmail.com>

 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 * ***** END LICENSE BLOCK *****
 *
 * OpenPGP MIME Secure Email Zimlet
 *
 * Written by Nguyen Van Nguyen <nguyennv1981@gmail.com>
 */

/**
 * Key store constructor.
 */
OpenPGPSecureKeyStore = function(handler) {
    this._handler = handler;
    this._fingerprints = [];
    this._addCallbacks = [];
    this._removeCallbacks = [];

    var uid = this._handler.getUserID();
    this._privateKeyItem = 'openpgp-secure-private-key-' + uid;
    this._passphraseItem = 'openpgp-secure-passphrase-' + uid;
    this._publicKeysItem = 'openpgp-secure-public-keys-' + uid;

    this.passphrase = '';
    this.privateKey = false;
    this.publicKeys = [];
    this.contactPublicKeys = [];
    this.globalPublicKeys = [];
};

OpenPGPSecureKeyStore.ADD_CALLBACK = 'add';
OpenPGPSecureKeyStore.REMOVE_CALLBACK = 'remove';

OpenPGPSecureKeyStore.prototype = new Object();
OpenPGPSecureKeyStore.prototype.constructor = OpenPGPSecureKeyStore;

/**
 * Initialize key store.
 */
OpenPGPSecureKeyStore.prototype.init = function() {
    var self = this;

    var sequence = Promise.resolve();
    return sequence.then(function() {
        return self.setPublicKeys(self._readPublicKeys());
    })
    .then(function() {
        self._scanContacts();
        self._globalTrust();
        return;
    })
    .then(function() {
        return OpenPGPUtils.localStorageRead(
            self._passphraseItem,
            self._handler.getSecurePassword()
        )
        .then(function(passphrase) {
            return passphrase;
        });
    })
    .then(function(passphrase) {
        if (passphrase) {
            return OpenPGPUtils.localStorageRead(
                self._privateKeyItem,
                self._handler.getSecurePassword()
            )
            .then(function(privateKey) {
                if (privateKey) {
                    var privKey = openpgp.key.readArmored(privateKey);
                    privKey.keys.forEach(function(key) {
                        if (key.decrypt(passphrase)) {
                            self.privateKey = key;
                            self.passphrase = passphrase;
                        }
                        else {
                            throw new Error(self._handler.getMessage('decryptPrivateKeyError'));
                        }
                    });
                }
                return self.privateKey;
            });
        }
        else {
            return self.privateKey;
        }
    })
    .then(function(privateKey) {
        if (privateKey) {
            self.addPublicKey(privateKey.toPublic());
        }
    });
};

/**
 * Add public key to key store.
 *
 * @param {Key} key OpenPGP public key
 */
OpenPGPSecureKeyStore.prototype.addPublicKey = function(key) {
    if (key.isPublic()) {
        var self = this;
        var added = false;

        var fingerprint = key.primaryKey.getFingerprint();
        if (!self._fingerprints[fingerprint]) {
            self._fingerprints[fingerprint] = fingerprint;
            self.publicKeys.push(key);
            added = true;
        }

        if (added) {
            this._addCallbacks.forEach(function(callback) {
                callback.run(key);
            });
            this._storePublicKeys();
        }
    }
};

/**
 * Remove public key from key store.
 *
 * @param {String} fingerprint OpenPGP key fingerprint
 */
OpenPGPSecureKeyStore.prototype.removePublicKey = function(fingerprint) {
    var self = this;
    var removed = false;
    this.publicKeys.forEach(function(key, index) {
        if (fingerprint == key.primaryKey.getFingerprint()) {
            self.publicKeys.splice(index, 1);
            delete self._fingerprints[fingerprint];
            removed = true;
        }
    });

    if (removed) {
        this._removeCallbacks.forEach(function(callback) {
            callback.run(fingerprint);
        });
        this._storePublicKeys();
    }
};

/**
 * Add callback to key store.
 *
 * @param {AjxCallback} callback
 * @param {String} type Callback type [add or remove]
 */
OpenPGPSecureKeyStore.prototype.addCallback = function(callback, type) {
    if (type === OpenPGPSecureKeyStore.REMOVE_CALLBACK) {
        this._removeCallbacks.push(callback);
    }
    else {
        this._addCallbacks.push(callback);
    }
};

/**
 * Get passphrase of private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPassphrase = function() {
    return this.passphrase;
};

/**
 * Get private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPrivateKey = function() {
    return this.privateKey;
};

/**
 * Set private key to key store.
 *
 * @param {String} privateKey Armored private key
 * @param {String} passphrase Passphrase for key decrypting
 */
OpenPGPSecureKeyStore.prototype.setPrivateKey = function(privateKey, passphrase) {
    var self = this;
    var privKey = openpgp.key.readArmored(privateKey);
    privKey.keys.forEach(function(key) {
        if (key.decrypt(passphrase)) {
            self.privateKey = key;
            self.passphrase = passphrase;
            self.addPublicKey(key.toPublic());

            OpenPGPUtils.localStorageSave(
                self._privateKeyItem,
                self._handler.getSecurePassword(),
                privateKey
            );
            OpenPGPUtils.localStorageSave(
                self._passphraseItem,
                self._handler.getSecurePassword(),
                passphrase
            );
        }
        else {
            throw new Error(this._handler.getMessage('decryptPrivateKeyError'));
        }
    });
    return this;
};

/**
 * Get public key from private key in key store.
 */
OpenPGPSecureKeyStore.prototype.getPublicKey = function() {
    return this.privateKey ? this.privateKey.toPublic() : false;
};

/**
 * Get public keys from store.
 */
OpenPGPSecureKeyStore.prototype.getPublicKeys = function() {
    return this.publicKeys;
};

/**
 * Get all public keys.
 */
OpenPGPSecureKeyStore.prototype.getAllPublicKeys = function() {
    var publicKeys = this.publicKeys;
    return publicKeys.concat(this.contactPublicKeys).concat(this.globalPublicKeys);
};

/**
 * Set all public keys to key store.
 *
 * @param {String} armoredKeys An armored public keys
 */
OpenPGPSecureKeyStore.prototype.setPublicKeys = function(armoredKeys) {
    if (armoredKeys) {
        var self = this;
        this.publicKeys.splice(0);
        var pubKeys = openpgp.key.readArmored(armoredKeys);
        pubKeys.keys.forEach(function(key) {
            var fingerprint = key.primaryKey.getFingerprint();
            if (key.isPublic() && !self._fingerprints[fingerprint]) {
                self.publicKeys.push(key);
                self._fingerprints[fingerprint] = fingerprint;
            }
        });
    }
    return this;
};

/**
 * Get public keys matching with email addresss.
 *
 * @param {Array} addresses Email addresses to search for
 */
OpenPGPSecureKeyStore.prototype.havingPublicKeys = function(addresses) {
    var having = [];
    var dupes = [];
    var publicKeys = this.getAllPublicKeys();
    publicKeys.forEach(function(key) {
        var userIds = key.getUserIds();
        addresses.forEach(function(address) {
            userIds.forEach(function(uid) {
                var fingerprint = key.primaryKey.getFingerprint();
                if (uid.indexOf(address) >= 0 && !dupes[fingerprint + uid]) {
                    having.push(key);
                    dupes[fingerprint + uid] = fingerprint + uid;
                }
            });
        });
    });
    return having;
};

/**
 * Get email addesses missing public key.
 *
 * @param {Array} addresses Email addresses to search for
 */
OpenPGPSecureKeyStore.prototype.publicKeyMissing = function(addresses) {
    var uidAddresses = [];
    var publicKeys = this.getAllPublicKeys();
    publicKeys.forEach(function(key) {
        var userIds = key.getUserIds();
        userIds.forEach(function(uid) {
            var addr = AjxEmailAddress.parse(uid);
            if (addr) {
                uidAddresses[addr.getAddress()] = addr.getAddress();
            }
        });
    });

    var missing = [];
    addresses.forEach(function(address) {
        if (!uidAddresses[address]) {
            missing.push(address);
        }
    });
    return missing;
};

/**
 * Check public key is existed in key store.
 *
 * @param {String} fingerprint Fingerprint string of the key
 */
OpenPGPSecureKeyStore.prototype.publicKeyExisted = function(fingerprint) {
    return this._fingerprints[fingerprint] ? true : false;
};

/**
 * Export all public keys in store.
 */
OpenPGPSecureKeyStore.prototype.exportPublicKeys = function() {
    var packetlist = new openpgp.packet.List();
    this.publicKeys.forEach(function(key) {
        packetlist.read(key.toPacketlist().write());
    });
    return openpgp.armor.encode(openpgp.enums.armor.public_key, packetlist.write());
};

/**
 * Export all public keys.
 */
OpenPGPSecureKeyStore.prototype.exportAllPublicKeys = function() {
    var publicKeys = this.getAllPublicKeys();
    var packetlist = new openpgp.packet.List();
    publicKeys.forEach(function(key) {
        packetlist.read(key.toPacketlist().write());
    });
    return openpgp.armor.encode(openpgp.enums.armor.public_key, packetlist.write());
};

/**
 * Genarate public key information.
 */
OpenPGPSecureKeyStore.keyInfo = function(key) {
    var uids = [];
    var userIds = key.getUserIds();
    if (userIds) {
        userIds.forEach(function(uid) {
            uids.push(AjxStringUtil.htmlEncode(uid));
        });
    }

    var priKey = key.primaryKey;
    var keyLength = '';
    if (priKey.mpi.length > 0) {
        keyLength = (priKey.mpi[0].byteLength() * 8);
    }
    return {
        value: key.armor(),
        uids: uids,
        fingerprint: priKey.getFingerprint(),
        keyid: priKey.getKeyId().toHex(),
        algorithm: priKey.algorithm,
        keyLength: keyLength,
        created: priKey.created
    };
};

/**
 * Read all public keys from storage.
 */
OpenPGPSecureKeyStore.prototype._readPublicKeys = function() {
    return localStorage[this._publicKeysItem];
};

/**
 * Store all public keys to storage.
 */
OpenPGPSecureKeyStore.prototype._storePublicKeys = function() {
    localStorage[this._publicKeysItem] = this.exportPublicKeys();
    appCtxt.notifyZimlet(OpenPGPZimbraSecure.NAME, 'onPublicKeyChange');
};

/**
 * Scan public keys from user contacts.
 */
OpenPGPSecureKeyStore.prototype._scanContacts = function() {
    var self = this;
    var url = [
        OpenPGPUtils.restUrl(), '/contacts?fmt=csv'
    ].join('');

    var callback = new AjxCallback(function(response) {
        if (response.success) {
            var entries = response.text.split('"');
            entries.forEach(function(entry) {
                if (OpenPGPUtils.hasInlinePGPContent(entry, OpenPGPUtils.OPENPGP_PUBLIC_KEY_HEADER)) {
                    var contactKeys = openpgp.key.readArmored(entry);
                    contactKeys.keys.forEach(function(key) {
                        var fingerprint = key.primaryKey.getFingerprint();
                        if (key.isPublic() && !self._fingerprints[fingerprint]) {
                            self.contactPublicKeys.push(key);
                            self._fingerprints[fingerprint] = fingerprint;
                        }
                    });
                }
            });
        }
    });

    AjxRpc.invoke('', url, {}, callback, true);
};

/**
 * Load public keys from global trust.
 */
OpenPGPSecureKeyStore.prototype._globalTrust = function() {
    var resource = this._handler.getZimletContext().getConfig('global-trust');
    if (resource) {
        var self = this;
        var url = this._handler.getResource(resource);
        var callback = new AjxCallback(function(response) {
            if (response.success) {
                var globalKeys = openpgp.key.readArmored(response.text);
                globalKeys.keys.forEach(function(key) {
                    var fingerprint = key.primaryKey.getFingerprint();
                    if (key.isPublic() && !self._fingerprints[fingerprint]) {
                        self.globalPublicKeys.push(key);
                        self._fingerprints[fingerprint] = fingerprint;
                    }
                });
            }
        });

        AjxRpc.invoke('', url, {}, callback, true);
    }
};
<|endoftext|>


<commit_before>var mapzen = mapzen || {};
mapzen.whosonfirst = mapzen.whosonfirst || {};

mapzen.whosonfirst.properties = (function(){

    var self = {

	'render': function(props){

	    var possible_wof = [
		'wof.belongsto',
		'wof.parent_id', 'wof.children',
		'wof.breaches',
		'wof.supersedes',
		'wof.superseded_by',
		// TO DO : please to write js.whosonfirst.placetypes...
		'wof.hierarchy.continent_id', 'wof.hierarchy.country_id', 'wof.hierarchy.region_id',
		'wof.hierarchy.county_id', 'wof.hierarchy.locality_id', 'wof.hierarchy.neighbourhood_id',
		'wof.hierarchy.campus_id', 'wof.hierarchy.venue_id'
	    ];

	    var text_callbacks = {
		'wof.id': mapzen.whosonfirst.yesnofix.render_code,
		'wof.placetype': self.render_placetype,
		'wof.concordances.gn:id': self.render_geonames_id,
		'wof.concordances.gp:id': self.render_woedb_id,
		'wof.concordances.woe:id': self.render_woedb_id,
		'wof.concordances.tgn:id': self.render_tgn_id,
		'wof.lastmodified': mapzen.whosonfirst.yesnofix.render_timestamp,
		'wof.megacity': self.render_megacity,
		'wof.tags': self.render_wof_tags,
		'wof.name': self.render_wof_name,
		'sg.city': self.render_simplegeo_city,
		'sg.postcode': self.render_simplegeo_postcode,
		'sg.tags': self.render_simplegeo_tags,
		'sg.classifier': self.render_simplegeo_classifiers,
	    };

	    var text_renderers = function(d, ctx){

		if ((possible_wof.indexOf(ctx) != -1) && (d > 0)){
		    return self.render_wof_id;
		}

		else if (ctx.match(/^name-/)){
		    return self.render_wof_name;
		}

		else if (ctx.match(/^sg-classifiers-/)){
		    return self.render_simplegeo_classifiers;
		}

		else if (text_callbacks[ctx]){
		    return text_callbacks[ctx];
		}

		else {
		    return null;
		}
	    };

	    var dict_mappings = {
		'wof.concordances.dbp:id': 'dbpedia',
		'wof.concordances.fb:id': 'freebase',
		'wof.concordances.fct:id': 'factual',
		'wof.concordances.gp:id': 'geonames',
		'wof.concordances.gn:id': 'geoplanet',
		'wof.concordances.loc:id': 'library of congress',
		'wof.concordances.nyt:id': 'new york times',
		'wof.concordances.wd:id': 'wikidata',
		// please build me on the fly using mz.wof.placetypes
		'wof.hierarchy.continent_id': 'continent',
		'wof.hierarchy.country_id': 'country',
		'wof.hierarchy.region_id': 'region',
		'wof.hierarchy.county_id': 'county',
		'wof.hierarchy.locality_id': 'locality',
		'wof.hierarchy.neighbourhood_id': 'neighbourhood',
	    };

	    var dict_renderers = function(d, ctx){

		// TO DO: something to match 3-letter language code + "_x_" + suffix
		// or more specifically something to match/ convert 3-letter language
		// codes wrapped up in a handy library (20160211/thisisaaronland)

		if (dict_mappings[ctx]){
		    return function(){
			return dict_mappings[ctx];
		    };
		}

		return null;
	    };

	    var text_exclusions = function(d, ctx){

		return function(){

		    if (ctx.match(/^geom/)){
			return true;
		    }

		    else if ((ctx.match(/^edtf/)) && (d == "uuuu")){
			return true;
		    }

		    else if (ctx == 'wof.lastmodified'){
			return true;
		    }

		    else {
			return false;
		    }
		};

	    };

	    mapzen.whosonfirst.yesnofix.set_custom_renderers('text', text_renderers);
	    mapzen.whosonfirst.yesnofix.set_custom_renderers('dict', dict_renderers);

	    mapzen.whosonfirst.yesnofix.set_custom_exclusions('text', text_exclusions);

	    var pretty = mapzen.whosonfirst.yesnofix.engage(props);
	    return pretty;
	},

	// TO DO : make 'mapzen.whosonfirst.spelunker.abs_root_url' something like
	// 'mapzen.whosonfirst.common.abs_root_url' or equivalent...

	'render_wof_id': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "id/" + encodeURIComponent(d) + "/";
	    var el = mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);

	    var text = el.children[0];
	    text.setAttribute("data-value", mapzen.whosonfirst.php.htmlspecialchars(d));
	    text.setAttribute("class", "props-uoc props-uoc-name props-uoc-name_" + mapzen.whosonfirst.php.htmlspecialchars(d));

	    return el;

	},

	'render_wof_placetype': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "placetypes/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_geonames_id': function(d, ctx){
	    var link = "http://geonames.org/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_woedb_id': function(d, ctx){
	    var link = "https://woe.spum.org/id/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_tgn_id': function(d, ctx){
	    var link = "http://vocab.getty.edu/tgn/" + encodeURIComponent(d);
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_megacity': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "megacities/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, "HOW BIG WOW MEGA SO CITY", ctx);
	},

	'render_wof_tags': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "tags/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_wof_name': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "search/?q=" + encodeURIComponent(d);
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_city': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "search/?q=" + encodeURIComponent(d) + "&placetype=locality";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_postcode': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "postalcodes/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_classifiers': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "categories/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_tags': function(d, ctx){
            var root = mapzen.whosonfirst.spelunker.abs_root_url();
            var link = root + "tags/" + encodeURIComponent(d) + "/";
            return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},
    };

    return self;

})();
<commit_msg>trigger the correct renderer for geonames/geoplanet<commit_after>var mapzen = mapzen || {};
mapzen.whosonfirst = mapzen.whosonfirst || {};

mapzen.whosonfirst.properties = (function(){

    var self = {

	'render': function(props){

	    var possible_wof = [
		'wof.belongsto',
		'wof.parent_id', 'wof.children',
		'wof.breaches',
		'wof.supersedes',
		'wof.superseded_by',
		// TO DO : please to write js.whosonfirst.placetypes...
		'wof.hierarchy.continent_id', 'wof.hierarchy.country_id', 'wof.hierarchy.region_id',
		'wof.hierarchy.county_id', 'wof.hierarchy.locality_id', 'wof.hierarchy.neighbourhood_id',
		'wof.hierarchy.campus_id', 'wof.hierarchy.venue_id'
	    ];

	    var text_callbacks = {
		'wof.id': mapzen.whosonfirst.yesnofix.render_code,
		'wof.placetype': self.render_placetype,
		'wof.concordances.gn:id': self.render_geonames_id,
		'wof.concordances.gp:id': self.render_woedb_id,
		'wof.concordances.woe:id': self.render_woedb_id,
		'wof.concordances.tgn:id': self.render_tgn_id,
		'wof.lastmodified': mapzen.whosonfirst.yesnofix.render_timestamp,
		'wof.megacity': self.render_megacity,
		'wof.tags': self.render_wof_tags,
		'wof.name': self.render_wof_name,
		'sg.city': self.render_simplegeo_city,
		'sg.postcode': self.render_simplegeo_postcode,
		'sg.tags': self.render_simplegeo_tags,
		'sg.classifier': self.render_simplegeo_classifiers,
	    };

	    var text_renderers = function(d, ctx){

		if ((possible_wof.indexOf(ctx) != -1) && (d > 0)){
		    return self.render_wof_id;
		}

		else if (ctx.match(/^name-/)){
		    return self.render_wof_name;
		}

		else if (ctx.match(/^sg-classifiers-/)){
		    return self.render_simplegeo_classifiers;
		}

		else if (text_callbacks[ctx]){
		    return text_callbacks[ctx];
		}

		else {
		    return null;
		}
	    };

	    var dict_mappings = {
		'wof.concordances.dbp:id': 'dbpedia',
		'wof.concordances.fb:id': 'freebase',
		'wof.concordances.fct:id': 'factual',
		'wof.concordances.gn:id': 'geonames',
		'wof.concordances.gp:id': 'geoplanet',
		'wof.concordances.loc:id': 'library of congress',
		'wof.concordances.nyt:id': 'new york times',
		'wof.concordances.wd:id': 'wikidata',
		// please build me on the fly using mz.wof.placetypes
		'wof.hierarchy.continent_id': 'continent',
		'wof.hierarchy.country_id': 'country',
		'wof.hierarchy.region_id': 'region',
		'wof.hierarchy.county_id': 'county',
		'wof.hierarchy.locality_id': 'locality',
		'wof.hierarchy.neighbourhood_id': 'neighbourhood',
	    };

	    var dict_renderers = function(d, ctx){

		// TO DO: something to match 3-letter language code + "_x_" + suffix
		// or more specifically something to match/ convert 3-letter language
		// codes wrapped up in a handy library (20160211/thisisaaronland)

		if (dict_mappings[ctx]){
		    return function(){
			return dict_mappings[ctx];
		    };
		}

		return null;
	    };

	    var text_exclusions = function(d, ctx){

		return function(){

		    if (ctx.match(/^geom/)){
			return true;
		    }

		    else if ((ctx.match(/^edtf/)) && (d == "uuuu")){
			return true;
		    }

		    else if (ctx == 'wof.lastmodified'){
			return true;
		    }

		    else {
			return false;
		    }
		};

	    };

	    mapzen.whosonfirst.yesnofix.set_custom_renderers('text', text_renderers);
	    mapzen.whosonfirst.yesnofix.set_custom_renderers('dict', dict_renderers);

	    mapzen.whosonfirst.yesnofix.set_custom_exclusions('text', text_exclusions);

	    var pretty = mapzen.whosonfirst.yesnofix.engage(props);
	    return pretty;
	},

	// TO DO : make 'mapzen.whosonfirst.spelunker.abs_root_url' something like
	// 'mapzen.whosonfirst.common.abs_root_url' or equivalent...

	'render_wof_id': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "id/" + encodeURIComponent(d) + "/";
	    var el = mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);

	    var text = el.children[0];
	    text.setAttribute("data-value", mapzen.whosonfirst.php.htmlspecialchars(d));
	    text.setAttribute("class", "props-uoc props-uoc-name props-uoc-name_" + mapzen.whosonfirst.php.htmlspecialchars(d));

	    return el;

	},

	'render_wof_placetype': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "placetypes/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_geonames_id': function(d, ctx){
	    var link = "http://geonames.org/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_woedb_id': function(d, ctx){
	    var link = "https://woe.spum.org/id/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_tgn_id': function(d, ctx){
	    var link = "http://vocab.getty.edu/tgn/" + encodeURIComponent(d);
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_megacity': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "megacities/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, "HOW BIG WOW MEGA SO CITY", ctx);
	},

	'render_wof_tags': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "tags/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_wof_name': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "search/?q=" + encodeURIComponent(d);
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_city': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "search/?q=" + encodeURIComponent(d) + "&placetype=locality";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_postcode': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "postalcodes/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_classifiers': function(d, ctx){
	    var root = mapzen.whosonfirst.spelunker.abs_root_url();
	    var link = root + "categories/" + encodeURIComponent(d) + "/";
	    return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},

	'render_simplegeo_tags': function(d, ctx){
            var root = mapzen.whosonfirst.spelunker.abs_root_url();
            var link = root + "tags/" + encodeURIComponent(d) + "/";
            return mapzen.whosonfirst.yesnofix.render_link(link, d, ctx);
	},
    };

    return self;

})();
<|endoftext|>


<commit_before>/*
 * logs.js: Commands related to user resources
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var jitsu = require('../../jitsu'),
    util = require('util'),
    dateformat = require('dateformat'),
    logs = exports,
    utile = jitsu.common;

logs.usage = [
  'The `jitsu logs` command will display logs related to the app',
  'The default number of lines to show is 10',
  '',
  'Example usages:',
  'jitsu logs app <app name>',
  'jitsu logs app <app name> <number of lines to show>'
];

//
// ### function app (appName, callback)
// #### @appName {string} the application to get the logs for
// #### @callback {function} Continuation to pass control to when complete.
// Queries the log API and retrieves the logs for the specified application
//
logs.app = function (appName, amount, callback) {
  //
  // This is defined so that it can get called once all the arguments are
  // sorted out.
  //

  //
  // Allows arbitrary amount of arguments
  //
  if (arguments.length) {
    var args = utile.args(arguments);
    callback = args.callback;
    appName  = args[0] || null;
    amount   = args[1] || null;
  }

  function byApp(appName, amount, callback) {
    jitsu.apps.view(appName, function (err) {
      if (err) {
        return err.statusCode === 404
          ? callback(new Error('Application not found.'), true)
          : callback(err);
      }

      jitsu.logs.byApp(appName, amount, function (err, results) {
        if (err) {
          return callback(err);
        }

        jitsu.log.info('Listing logs for ' + appName.magenta);

        putLogs(results, appName, amount);
        callback();
      });
    });
  }

  function getAppName(callback) {
    jitsu.package.read(process.cwd(), function (err, pkg) {
      if (!err) {
        jitsu.log.info('Attempting to load logs for ' + (process.cwd()+ '/package.json').grey);
        return callback(null, pkg.name);
      }
      callback(err);
    });
  }

  amount = amount || 100;

  if (!appName) {
    getAppName(function (err, name) {
      if (err) {
        jitsu.commands.list(function () {
          jitsu.log.info('Which application to view ' + 'logs'.magenta + ' for?');
          jitsu.prompt.get(["app name"], function (err, result) {
            if (err) {
              jitsu.log.error('Prompt error:');
              return callback(err);
            }
            appName = result["app name"];
            byApp(appName, amount, callback);
          });
        })
      } else {
        byApp(name, amount, callback);
      }
    });
  } else {
     byApp(appName, amount, callback);
  }

}

logs.app.usage = [
  'Print the logs from specified application. The default number of',
  'lines to show is 10.',
  'jits logs app <app name> <number of lines to show>',
  '',
  'Example usage:',
  'jitsu logs app test',
  'jitsu logs app test 40'
];

//
// ### function putLogs (results, appName, amount, showApp)
// #### @results {Object} Logs object to output.
// #### @appName {string} App name associated with the log text.
// #### @showApp {boolean} Value indicating if the app name should be output.
// Parses, formats, and outputs the specified `results` to the user.
//
function putLogs (results, appName, amount, showApp) { //TODO: utilize amount and showApp
  //
  // Allows arbitrary amount of arguments
  //
  if (arguments.length) {
    var args = utile.args(arguments);
    results  = args[0] || null;
    appName  = args[1] || null;
    amount   = args[2] || null;
    showApp  = args[3] || null;
  }

  if(appName.indexOf('/') !== -1){
    //username = appName.split('/')[0];
    appName = appName.split('/')[1];
  }

  if (results.length === 0) {
    return jitsu.log.warn('No logs for ' + appName.magenta + ' in specified timespan');
  }

  var logLength = jitsu.config.get('loglength'),
      logged = 0;

  function sort(first, second) {
    return first.time - second.time;
  }

  results.sort(sort).forEach(function (datum) {
    if (datum.description && datum.description !== null) {

      datum.description.split('\n').forEach(function (line) {
        var now = new Date(datum.time * 1000);
        now = dateformat(now, "mm/dd HH:MM:ss Z");

        var type = (datum.service === 'logs/stderr') ? "err".red : "out".green;

        if (line.length) {
          console.log(util.format('[%s][%s] %s', now.toString().yellow, type, line));
        }
      });
    }
  });
}
<commit_msg>[command] jitsu logs tail<commit_after>/*
 * logs.js: Commands related to user resources
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var jitsu = require('../../jitsu'),
    util = require('util'),
    dateformat = require('dateformat'),
    logs = exports,
    utile = jitsu.common;

logs.usage = [
  'The `jitsu logs` command will display logs related to the app',
  'The default number of lines to show is 10',
  '',
  'Example usages:',
  'jitsu logs app <app name>',
  'jitsu logs app <app name> <number of lines to show>'
];

//
// ### function tail (appName, callback)
// #### @appName {string} the application to get the logs for
// #### @callback {function} Continuation to pass control when complete.
// Queries the log API using live streaming
//
logs.tail = function (appName, callback) {
  //
  // This is defined so that it can get called once all the arguments are
  // sorted out.
  //

  //
  // Allows arbitrary amount of arguments
  //
  if (arguments.length) {
    var args = utile.args(arguments);
    callback = args.callback;
    appName  = args[0] || null;
  }

  function tail(appName, callback) {
    jitsu.apps.view(appName, function (err) {
      if (err) {
        return err.statusCode === 404
          ? callback(new Error('Application not found.'), true)
          : callback(err);
      }

      jitsu.logs.live(appName, function (socket) {
        jitsu.log.info('Listing logs for ' + appName.magenta);
        socket.on('data', printLog);
      });
    });
  }

  function getAppName(callback) {
    jitsu.package.read(process.cwd(), function (err, pkg) {
      if (!err) {
        jitsu.log.info('Attempting to load logs for ' + (process.cwd()+ '/package.json').grey);
        return callback(null, pkg.name);
      }
      callback(err);
    });
  }

  if (!appName) {
    getAppName(function (err, name) {
      if (err) {
        jitsu.commands.list(function () {
          jitsu.log.info('Which application to view ' + 'logs'.magenta + ' for?');
          jitsu.prompt.get(["app name"], function (err, result) {
            if (err) {
              jitsu.log.error('Prompt error:');
              return callback(err);
            }
            appName = result["app name"];
            tail(appName, callback);
          });
        })
      } else {
        tail(name, callback);
      }
    });
  } else {
     tail(appName, callback);
  }
};

logs.tail.usage = [
  'The `jitsu logs tail` command will display logs in a live mode',
  'jitsu logs tail <app name>',
  '',
  'Example usages:',
  'jitsu logs tail',
  'jitsu logs tail app'
];

//
// ### function app (appName, callback)
// #### @appName {string} the application to get the logs for
// #### @callback {function} Continuation to pass control to when complete.
// Queries the log API and retrieves the logs for the specified application
//
logs.app = function (appName, amount, callback) {
  //
  // This is defined so that it can get called once all the arguments are
  // sorted out.
  //

  //
  // Allows arbitrary amount of arguments
  //
  if (arguments.length) {
    var args = utile.args(arguments);
    callback = args.callback;
    appName  = args[0] || null;
    amount   = args[1] || null;
  }

  function byApp(appName, amount, callback) {
    jitsu.apps.view(appName, function (err) {
      if (err) {
        return err.statusCode === 404
          ? callback(new Error('Application not found.'), true)
          : callback(err);
      }

      jitsu.logs.byApp(appName, amount, function (err, results) {
        if (err) {
          return callback(err);
        }

        jitsu.log.info('Listing logs for ' + appName.magenta);

        putLogs(results, appName, amount);
        callback();
      });
    });
  }

  function getAppName(callback) {
    jitsu.package.read(process.cwd(), function (err, pkg) {
      if (!err) {
        jitsu.log.info('Attempting to load logs for ' + (process.cwd()+ '/package.json').grey);
        return callback(null, pkg.name);
      }
      callback(err);
    });
  }

  amount = amount || 10;

  if (!appName) {
    getAppName(function (err, name) {
      if (err) {
        jitsu.commands.list(function () {
          jitsu.log.info('Which application to view ' + 'logs'.magenta + ' for?');
          jitsu.prompt.get(["app name"], function (err, result) {
            if (err) {
              jitsu.log.error('Prompt error:');
              return callback(err);
            }
            appName = result["app name"];
            byApp(appName, amount, callback);
          });
        })
      } else {
        byApp(name, amount, callback);
      }
    });
  } else {
     byApp(appName, amount, callback);
  }

}

logs.app.usage = [
  'Print the logs from specified application. The default number of',
  'lines to show is 10.',
  'jits logs app <app name> <number of lines to show>',
  '',
  'Example usage:',
  'jitsu logs app test',
  'jitsu logs app test 40'
];

//
// ### function putLogs (results, appName, amount, showApp)
// #### @results {Object} Logs object to output.
// #### @appName {string} App name associated with the log text.
// #### @showApp {boolean} Value indicating if the app name should be output.
// Parses, formats, and outputs the specified `results` to the user.
//
function putLogs (results, appName) {
  //
  // Allows arbitrary amount of arguments
  //
  if (arguments.length) {
    var args = utile.args(arguments);
    results  = args[0] || null;
    appName  = args[1] || null;
    amount   = args[2] || null;
    showApp  = args[3] || null;
  }

  if(appName.indexOf('/') !== -1){
    //username = appName.split('/')[0];
    appName = appName.split('/')[1];
  }

  if (results.length === 0) {
    return jitsu.log.warn('No logs for ' + appName.magenta + ' in specified timespan');
  }

  var logLength = jitsu.config.get('loglength'),
      logged = 0;

  function sort(first, second) {
    return first.time - second.time;
  }

  results.sort(sort).forEach(printLog);
}

function printLog(datum) {
  if (datum.description && datum.description !== null) {
    datum.description.split('\n').forEach(function (line) {
      var now = new Date(datum.time * 1000);
      now = dateformat(now, "mm/dd HH:MM:ss Z");

      var type = (datum.service === 'logs/stderr') ? "err".red : "out".green;

      if (line.length) {
        console.log(util.format('[%s][%s] %s', now.toString().yellow, type, line));
      }
    });
  }
}
<|endoftext|>


<commit_before>!function(a){function b(){return"Markdown.mk_block( "+uneval(this.toString())+", "+uneval(this.trailing)+", "+uneval(this.lineNumber)+" )"}function c(){var a=require("util");return"Markdown.mk_block( "+a.inspect(this.toString())+", "+a.inspect(this.trailing)+", "+a.inspect(this.lineNumber)+" )"}function d(a){for(var b=0,c=-1;-1!==(c=a.indexOf("\n",c+1));)b++;return b}function e(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function f(a){if("string"==typeof a)return e(a);var b=a.shift(),c={},d=[];for(!a.length||"object"!=typeof a[0]||a[0]instanceof Array||(c=a.shift());a.length;)d.push(f(a.shift()));var g="";for(var h in c)g+=" "+h+'="'+e(c[h])+'"';return"img"===b||"br"===b||"hr"===b?"<"+b+g+"/>":"<"+b+g+">"+d.join("")+"</"+b+">"}function g(a,b,c){var d;c=c||{};var e=a.slice(0);"function"==typeof c.preprocessTreeNode&&(e=c.preprocessTreeNode(e,b));var f=o(e);if(f){e[1]={};for(d in f)e[1][d]=f[d];f=e[1]}if("string"==typeof e)return e;switch(e[0]){case"header":e[0]="h"+e[1].level,delete e[1].level;break;case"bulletlist":e[0]="ul";break;case"numberlist":e[0]="ol";break;case"listitem":e[0]="li";break;case"para":e[0]="p";break;case"markdown":e[0]="html",f&&delete f.references;break;case"code_block":e[0]="pre",d=f?2:1;var h=["code"];h.push.apply(h,e.splice(d,e.length-d)),e[d]=h;break;case"inlinecode":e[0]="code";break;case"img":e[1].src=e[1].href,delete e[1].href;break;case"linebreak":e[0]="br";break;case"link":e[0]="a";break;case"link_ref":e[0]="a";var i=b[f.ref];if(!i)return f.original;delete f.ref,f.href=i.href,i.title&&(f.title=i.title),delete f.original;break;case"img_ref":e[0]="img";var i=b[f.ref];if(!i)return f.original;delete f.ref,f.src=i.href,i.title&&(f.title=i.title),delete f.original}if(d=1,f){for(var j in e[1]){d=2;break}1===d&&e.splice(d,1)}for(;d<e.length;++d)e[d]=g(e[d],b,c);return e}function h(a){for(var b=o(a)?2:1;b<a.length;)"string"==typeof a[b]?b+1<a.length&&"string"==typeof a[b+1]?a[b]+=a.splice(b+1,1)[0]:++b:(h(a[b]),++b)}function i(a,b){function c(a){this.len_after=a,this.name="close_"+b}var d=a+"_state",e="strong"===a?"em_state":"strong_state";return function(f){if(this[d][0]===b)return this[d].shift(),[f.length,new c(f.length-b.length)];var g=this[e].slice(),h=this[d].slice();this[d].unshift(b);var i=this.processInline(f.substr(b.length)),j=i[i.length-1];if(this[d].shift(),j instanceof c){i.pop();var k=f.length-j.len_after;return[k,[a].concat(i)]}return this[e]=g,this[d]=h,[b.length,b]}}function j(a){for(var b=a.split(""),c=[""],d=!1;b.length;){var e=b.shift();switch(e){case" ":d?c[c.length-1]+=e:c.push("");break;case"'":case'"':d=!d;break;case"\\":e=b.shift();default:c[c.length-1]+=e}}return c}var k={};k.mk_block=function(a,d,e){1===arguments.length&&(d="\n\n");var f=new String(a);return f.trailing=d,f.inspect=c,f.toSource=b,void 0!==e&&(f.lineNumber=e),f};var l=k.isArray=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)};k.forEach=Array.prototype.forEach?function(a,b,c){return a.forEach(b,c)}:function(a,b,c){for(var d=0;d<a.length;d++)b.call(c||a,a[d],d,a)},k.isEmpty=function(a){for(var b in a)if(hasOwnProperty.call(a,b))return!1;return!0},k.extract_attr=function(a){return l(a)&&a.length>1&&"object"==typeof a[1]&&!l(a[1])?a[1]:void 0};var m=function(a){switch(typeof a){case"undefined":this.dialect=m.dialects.Gruber;break;case"object":this.dialect=a;break;default:if(!(a in m.dialects))throw new Error("Unknown Markdown dialect '"+String(a)+"'");this.dialect=m.dialects[a]}this.em_state=[],this.strong_state=[],this.debug_indent=""};m.dialects={};var n=m.mk_block=k.mk_block,l=k.isArray;m.parse=function(a,b){var c=new m(b);return c.toTree(a)},m.prototype.split_blocks=function(a){a=a.replace(/(\r\n|\n|\r)/g,"\n");var b,c=/([\s\S]+?)($|\n#|\n(?:\s*\n|$)+)/g,e=[],f=1;for(null!==(b=/^(\s*\n)/.exec(a))&&(f+=d(b[0]),c.lastIndex=b[0].length);null!==(b=c.exec(a));)"\n#"===b[2]&&(b[2]="\n",c.lastIndex--),e.push(n(b[1],b[2],f)),f+=d(b[0]);return e},m.prototype.processBlock=function(a,b){var c=this.dialect.block,d=c.__order__;if("__call__"in c)return c.__call__.call(this,a,b);for(var e=0;e<d.length;e++){var f=c[d[e]].call(this,a,b);if(f)return(!l(f)||f.length>0&&!l(f[0]))&&this.debug(d[e],"didn't return a proper array"),f}return[]},m.prototype.processInline=function(a){return this.dialect.inline.__call__.call(this,String(a))},m.prototype.toTree=function(a,b){var c=a instanceof Array?a:this.split_blocks(a),d=this.tree;try{for(this.tree=b||this.tree||["markdown"];c.length;){var e=this.processBlock(c.shift(),c);e.length&&this.tree.push.apply(this.tree,e)}return this.tree}finally{b&&(this.tree=d)}},m.prototype.debug=function(){var a=Array.prototype.slice.call(arguments);a.unshift(this.debug_indent),"undefined"!=typeof print&&print.apply(print,a),"undefined"!=typeof console&&"undefined"!=typeof console.log&&console.log.apply(null,a)},m.prototype.loop_re_over_block=function(a,b,c){for(var d,e=b.valueOf();e.length&&null!==(d=a.exec(e));)e=e.substr(d[0].length),c.call(this,d);return e},m.buildBlockOrder=function(a){var b=[];for(var c in a)"__order__"!==c&&"__call__"!==c&&b.push(c);a.__order__=b},m.buildInlinePatterns=function(a){var b=[];for(var c in a)if(!c.match(/^__.*__$/)){var d=c.replace(/([\\.*+?|()\[\]{}])/g,"\\$1").replace(/\n/,"\\n");b.push(1===c.length?d:"(?:"+d+")")}b=b.join("|"),a.__patterns__=b;var e=a.__call__;a.__call__=function(a,c){return void 0!==c?e.call(this,a,c):e.call(this,a,b)}};var o=k.extract_attr;m.renderJsonML=function(a,b){b=b||{},b.root=b.root||!1;var c=[];if(b.root)c.push(f(a));else for(a.shift(),!a.length||"object"!=typeof a[0]||a[0]instanceof Array||a.shift();a.length;)c.push(f(a.shift()));return c.join("\n\n")},m.toHTMLTree=function(a,b,c){"string"==typeof a&&(a=this.parse(a,b));var d=o(a),e={};d&&d.references&&(e=d.references);var f=g(a,e,c);return h(f),f},m.toHTML=function(a,b,c){var d=this.toHTMLTree(a,b,c);return this.renderJsonML(d)};var p={};p.inline_until_char=function(a,b){for(var c=0,d=[];;){if(a.charAt(c)===b)return c++,[c,d];if(c>=a.length)return null;var e=this.dialect.inline.__oneElement__.call(this,a.substr(c));c+=e[0],d.push.apply(d,e.slice(1))}},p.subclassDialect=function(a){function b(){}function c(){}return b.prototype=a.block,c.prototype=a.inline,{block:new b,inline:new c}};var q=k.forEach,o=k.extract_attr,n=k.mk_block,r=k.isEmpty,s=p.inline_until_char,t={block:{atxHeader:function(a,b){var c=a.match(/^(#{1,6})\s*(.*?)\s*#*\s*(?:\n|$)/);if(!c)return void 0;var d=["header",{level:c[1].length}];return Array.prototype.push.apply(d,this.processInline(c[2])),c[0].length<a.length&&b.unshift(n(a.substr(c[0].length),a.trailing,a.lineNumber+2)),[d]},setextHeader:function(a,b){var c=a.match(/^(.*)\n([-=])\2\2+(?:\n|$)/);if(!c)return void 0;var d="="===c[2]?1:2,e=["header",{level:d},c[1]];return c[0].length<a.length&&b.unshift(n(a.substr(c[0].length),a.trailing,a.lineNumber+2)),[e]},code:function(a,b){var c=[],d=/^(?: {0,3}\t| {4})(.*)\n?/;if(!a.match(d))return void 0;a:for(;;){var e=this.loop_re_over_block(d,a.valueOf(),function(a){c.push(a[1])});if(e.length){b.unshift(n(e,a.trailing));break a}if(!b.length)break a;if(!b[0].match(d))break a;c.push(a.trailing.replace(/[^\n]/g,"").substring(2)),a=b.shift()}return[["code_block",c.join("\n")]]},horizRule:function(a,b){var c=a.match(/^(?:([\s\S]*?)\n)?[ \t]*([-_*])(?:[ \t]*\2){2,}[ \t]*(?:\n([\s\S]*))?$/);if(!c)return void 0;var d=[["hr"]];if(c[1]){var e=n(c[1],"",a.lineNumber);d.unshift.apply(d,this.toTree(e,[]))}return c[3]&&b.unshift(n(c[3],a.trailing,a.lineNumber+1)),d},lists:function(){function a(a){return new RegExp("(?:^("+i+"{0,"+a+"} {0,3})("+f+")\\s+)|"+"(^"+i+"{0,"+(a-1)+"}[ ]{0,4})")}function b(a){return a.replace(/ {0,3}\t/g,"    ")}function c(a,b,c,d){if(b)return a.push(["para"].concat(c)),void 0;var e=a[a.length-1]instanceof Array&&"para"===a[a.length-1][0]?a[a.length-1]:a;d&&a.length>1&&c.unshift(d);for(var f=0;f<c.length;f++){var g=c[f],h="string"==typeof g;h&&e.length>1&&"string"==typeof e[e.length-1]?e[e.length-1]+=g:e.push(g)}}function d(a,b){for(var c=new RegExp("^("+i+"{"+a+"}.*?\\n?)*$"),d=new RegExp("^"+i+"{"+a+"}","gm"),e=[];b.length>0&&c.exec(b[0]);){var f=b.shift(),g=f.replace(d,"");e.push(n(g,f.trailing,f.lineNumber))}return e}function e(a,b,c){var d=a.list,e=d[d.length-1];if(!(e[1]instanceof Array&&"para"===e[1][0]))if(b+1===c.length)e.push(["para"].concat(e.splice(1,e.length-1)));else{var f=e.pop();e.push(["para"].concat(e.splice(1,e.length-1)),f)}}var f="[*+-]|\\d+\\.",g=/[*+-]/,h=new RegExp("^( {0,3})("+f+")[ 	]+"),i="(?: {0,3}\\t| {4})";return function(f,i){function j(a){var b=g.exec(a[2])?["bulletlist"]:["numberlist"];return n.push({list:b,indent:a[1]}),b}var k=f.match(h);if(!k)return void 0;for(var l,m,n=[],o=j(k),p=!1,r=[n[0].list];;){for(var s=f.split(/(?=\n)/),t="",u="",v=0;v<s.length;v++){u="";var w=s[v].replace(/^\n/,function(a){return u=a,""}),x=a(n.length);if(k=w.match(x),void 0!==k[1]){t.length&&(c(l,p,this.processInline(t),u),p=!1,t=""),k[1]=b(k[1]);var y=Math.floor(k[1].length/4)+1;if(y>n.length)o=j(k),l.push(o),l=o[1]=["listitem"];else{var z=!1;for(m=0;m<n.length;m++)if(n[m].indent===k[1]){o=n[m].list,n.splice(m+1,n.length-(m+1)),z=!0;break}z||(y++,y<=n.length?(n.splice(y,n.length-y),o=n[y-1].list):(o=j(k),l.push(o))),l=["listitem"],o.push(l)}u=""}w.length>k[0].length&&(t+=u+w.substr(k[0].length))}t.length&&(c(l,p,this.processInline(t),u),p=!1,t="");var A=d(n.length,i);A.length>0&&(q(n,e,this),l.push.apply(l,this.toTree(A,[])));var B=i[0]&&i[0].valueOf()||"";if(!B.match(h)&&!B.match(/^ /))break;f=i.shift();var C=this.dialect.block.horizRule(f,i);if(C){r.push.apply(r,C);break}q(n,e,this),p=!0}return r}}(),blockquote:function(a,b){if(!a.match(/^>/m))return void 0;var c=[];if(">"!==a[0]){for(var d=a.split(/\n/),e=[],f=a.lineNumber;d.length&&">"!==d[0][0];)e.push(d.shift()),f++;var g=n(e.join("\n"),"\n",a.lineNumber);c.push.apply(c,this.processBlock(g,[])),a=n(d.join("\n"),a.trailing,f)}for(;b.length&&">"===b[0][0];){var h=b.shift();a=n(a+a.trailing+h,h.trailing,a.lineNumber)}var i=a.replace(/^> ?/gm,""),j=(this.tree,this.toTree(i,["blockquote"])),k=o(j);return k&&k.references&&(delete k.references,r(k)&&j.splice(1,1)),c.push(j),c},referenceDefn:function(a,b){var c=/^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;if(!a.match(c))return void 0;o(this.tree)||this.tree.splice(1,0,{});var d=o(this.tree);void 0===d.references&&(d.references={});var e=this.loop_re_over_block(c,a,function(a){a[2]&&"<"===a[2][0]&&">"===a[2][a[2].length-1]&&(a[2]=a[2].substring(1,a[2].length-1));var b=d.references[a[1].toLowerCase()]={href:a[2]};void 0!==a[4]?b.title=a[4]:void 0!==a[5]&&(b.title=a[5])});return e.length&&b.unshift(n(e,a.trailing)),[]},para:function(a){return[["para"].concat(this.processInline(a))]}},inline:{__oneElement__:function(a,b,c){var d,e;b=b||this.dialect.inline.__patterns__;var f=new RegExp("([\\s\\S]*?)("+(b.source||b)+")");if(d=f.exec(a),!d)return[a.length,a];if(d[1])return[d[1].length,d[1]];var e;return d[2]in this.dialect.inline&&(e=this.dialect.inline[d[2]].call(this,a.substr(d.index),d,c||[])),e=e||[d[2].length,d[2]]},__call__:function(a,b){function c(a){"string"==typeof a&&"string"==typeof e[e.length-1]?e[e.length-1]+=a:e.push(a)}for(var d,e=[];a.length>0;)d=this.dialect.inline.__oneElement__.call(this,a,b,e),a=a.substr(d.shift()),q(d,c);return e},"]":function(){},"}":function(){},__escape__:/^\\[\\`\*_{}\[\]()#\+.!\-]/,"\\":function(a){return this.dialect.inline.__escape__.exec(a)?[2,a.charAt(1)]:[1,"\\"]},"![":function(a){var b=a.match(/^!\[(.*?)\][ \t]*\([ \t]*([^")]*?)(?:[ \t]+(["'])(.*?)\3)?[ \t]*\)/);if(b){b[2]&&"<"===b[2][0]&&">"===b[2][b[2].length-1]&&(b[2]=b[2].substring(1,b[2].length-1)),b[2]=this.dialect.inline.__call__.call(this,b[2],/\\/)[0];var c={alt:b[1],href:b[2]||""};return void 0!==b[4]&&(c.title=b[4]),[b[0].length,["img",c]]}return b=a.match(/^!\[(.*?)\][ \t]*\[(.*?)\]/),b?[b[0].length,["img_ref",{alt:b[1],ref:b[2].toLowerCase(),original:b[0]}]]:[2,"!["]},"[":function v(a){var b=String(a),c=s.call(this,a.substr(1),"]");if(!c)return[1,"["];var v,d,e=1+c[0],f=c[1];a=a.substr(e);var g=a.match(/^\s*\([ \t]*([^"']*)(?:[ \t]+(["'])(.*?)\2)?[ \t]*\)/);if(g){var h=g[1];if(e+=g[0].length,h&&"<"===h[0]&&">"===h[h.length-1]&&(h=h.substring(1,h.length-1)),!g[3])for(var i=1,j=0;j<h.length;j++)switch(h[j]){case"(":i++;break;case")":0===--i&&(e-=h.length-j,h=h.substring(0,j))}return h=this.dialect.inline.__call__.call(this,h,/\\/)[0],d={href:h||""},void 0!==g[3]&&(d.title=g[3]),v=["link",d].concat(f),[e,v]}return g=a.match(/^\s*\[(.*?)\]/),g?(e+=g[0].length,d={ref:(g[1]||String(f)).toLowerCase(),original:b.substr(0,e)},v=["link_ref",d].concat(f),[e,v]):1===f.length&&"string"==typeof f[0]?(d={ref:f[0].toLowerCase(),original:b.substr(0,e)},v=["link_ref",d,f[0]],[e,v]):[1,"["]},"<":function(a){var b;return null!==(b=a.match(/^<(?:((https?|ftp|mailto):[^>]+)|(.*?@.*?\.[a-zA-Z]+))>/))?b[3]?[b[0].length,["link",{href:"mailto:"+b[3]},b[3]]]:"mailto"===b[2]?[b[0].length,["link",{href:b[1]},b[1].substr("mailto:".length)]]:[b[0].length,["link",{href:b[1]},b[1]]]:[1,"<"]},"`":function(a){var b=a.match(/(`+)(([\s\S]*?)\1)/);return b&&b[2]?[b[1].length+b[2].length,["inlinecode",b[3]]]:[1,"`"]},"  \n":function(){return[3,["linebreak"]]}}};t.inline["**"]=i("strong","**"),t.inline.__=i("strong","__"),t.inline["*"]=i("em","*"),t.inline._=i("em","_"),m.dialects.Gruber=t,m.buildBlockOrder(m.dialects.Gruber.block),m.buildInlinePatterns(m.dialects.Gruber.inline);var u=p.subclassDialect(t),o=k.extract_attr,q=k.forEach;u.processMetaHash=function(a){for(var b=j(a),c={},d=0;d<b.length;++d)if(/^#/.test(b[d]))c.id=b[d].substring(1);else if(/^\./.test(b[d]))c["class"]=c["class"]?c["class"]+b[d].replace(/./," "):b[d].substring(1);else if(/\=/.test(b[d])){var e=b[d].split(/\=/);c[e[0]]=e[1]}return c},u.block.document_meta=function(a){if(a.lineNumber>1)return void 0;if(!a.match(/^(?:\w+:.*\n)*\w+:.*$/))return void 0;o(this.tree)||this.tree.splice(1,0,{});var b=a.split(/\n/);for(var c in b){var d=b[c].match(/(\w+):\s*(.*)$/),e=d[1].toLowerCase(),f=d[2];this.tree[1][e]=f}return[]},u.block.block_meta=function(a){var b=a.match(/(^|\n) {0,3}\{:\s*((?:\\\}|[^\}])*)\s*\}$/);if(!b)return void 0;var c,d=this.dialect.processMetaHash(b[2]);if(""===b[1]){var e=this.tree[this.tree.length-1];if(c=o(e),"string"==typeof e)return void 0;c||(c={},e.splice(1,0,c));for(var f in d)c[f]=d[f];return[]}var g=a.replace(/\n.*$/,""),h=this.processBlock(g,[]);c=o(h[0]),c||(c={},h[0].splice(1,0,c));for(var f in d)c[f]=d[f];return h},u.block.definition_list=function(a,b){var c,d,e=/^((?:[^\s:].*\n)+):\s+([\s\S]+)$/,f=["dl"];if(!(d=a.match(e)))return void 0;for(var g=[a];b.length&&e.exec(b[0]);)g.push(b.shift());for(var h=0;h<g.length;++h){var d=g[h].match(e),i=d[1].replace(/\n$/,"").split(/\n/),j=d[2].split(/\n:\s+/);for(c=0;c<i.length;++c)f.push(["dt",i[c]]);for(c=0;c<j.length;++c)f.push(["dd"].concat(this.processInline(j[c].replace(/(\n)\s+/,"$1"))))}return[f]},u.block.table=function w(a){var b,c,d=function(a,b){b=b||"\\s",b.match(/^[\\|\[\]{}?*.+^$]$/)&&(b="\\"+b);for(var c,d=[],e=new RegExp("^((?:\\\\.|[^\\\\"+b+"])*)"+b+"(.*)");c=a.match(e);)d.push(c[1]),a=c[2];return d.push(a),d},e=/^ {0,3}\|(.+)\n {0,3}\|\s*([\-:]+[\-| :]*)\n((?:\s*\|.*(?:\n|$))*)(?=\n|$)/,f=/^ {0,3}(\S(?:\\.|[^\\|])*\|.*)\n {0,3}([\-:]+\s*\|[\-| :]*)\n((?:(?:\\.|[^\\|])*\|.*(?:\n|$))*)(?=\n|$)/;if(c=a.match(e))c[3]=c[3].replace(/^\s*\|/gm,"");else if(!(c=a.match(f)))return void 0;var w=["table",["thead",["tr"]],["tbody"]];c[2]=c[2].replace(/\|\s*$/,"").split("|");var g=[];for(q(c[2],function(a){a.match(/^\s*-+:\s*$/)?g.push({align:"right"}):a.match(/^\s*:-+\s*$/)?g.push({align:"left"}):a.match(/^\s*:-+:\s*$/)?g.push({align:"center"}):g.push({})}),c[1]=d(c[1].replace(/\|\s*$/,""),"|"),b=0;b<c[1].length;b++)w[1][1].push(["th",g[b]||{}].concat(this.processInline(c[1][b].trim())));return q(c[3].replace(/\|\s*$/gm,"").split("\n"),function(a){var c=["tr"];for(a=d(a,"|"),b=0;b<a.length;b++)c.push(["td",g[b]||{}].concat(this.processInline(a[b].trim())));w[2].push(c)},this),[w]},u.inline["{:"]=function(a,b,c){if(!c.length)return[2,"{:"];var d=c[c.length-1];if("string"==typeof d)return[2,"{:"];var e=a.match(/^\{:\s*((?:\\\}|[^\}])*)\s*\}/);if(!e)return[2,"{:"];var f=this.dialect.processMetaHash(e[1]),g=o(d);g||(g={},d.splice(1,0,g));for(var h in f)g[h]=f[h];return[e[0].length,""]},m.dialects.Maruku=u,m.dialects.Maruku.inline.__escape__=/^\\[\\`\*_{}\[\]()#\+.!\-|:]/,m.buildBlockOrder(m.dialects.Maruku.block),m.buildInlinePatterns(m.dialects.Maruku.inline),a.Markdown=m,a.parse=m.parse,a.toHTML=m.toHTML,a.toHTMLTree=m.toHTMLTree,a.renderJsonML=m.renderJsonML}(function(){return window.markdown={},window.markdown}());<commit_msg>Remove markdown.js<commit_after><|endoftext|>


<commit_before>const {
  Repository,
  Cred,
  Clone
} = require('nodegit');

const crypto = require('crypto');
const Change = require('./change');
const os = require('os');
const process = require('process');
const Error = require('@cardstack/plugin-utils/error');
const PendingChange = require('@cardstack/plugin-utils/pending-change');
const { promisify } = require('util');
const temp = require('temp').track();
const Gitchain = require('@cardstack/gitchain');
const log = require('@cardstack/logger')('cardstack/git');

const mkdir = promisify(temp.mkdir);

const pendingChanges = new WeakMap();

module.exports = class Writer {
  static create(params) {
    return new this(params);
  }
  constructor({ repo, idGenerator, basePath, branchPrefix, remote, hyperledger }) {
    this.repoPath = repo;
    this.basePath = basePath;
    this.branchPrefix = branchPrefix || "";
    this.repo = null;
    let hostname = os.hostname();
    this.myName = `PID${process.pid} on ${hostname}`;
    this.myEmail = `${os.userInfo().username}@${hostname}`;
    this.idGenerator = idGenerator;
    this.remote = remote;

    if (hyperledger) {
      let config = Object.assign({}, hyperledger);
      config.logger = log.info.bind(log); // TODO fix logger so we don't have to bind
      this.hyperledgerConfig = config;
    }

    if(remote) {
      this.fetchOpts = {
        callbacks: {
          credentials: (url, userName) => {
            if (remote && remote.privateKey) {
              return Cred.sshKeyMemoryNew(userName, remote.publicKey || '', remote.privateKey, remote.passphrase || '');
            }
            return Cred.sshKeyFromAgent(userName);
          }
        }
      };
    }
  }

  async prepareCreate(branch, session, type, document, isSchema) {
    return withErrorHandling(document.id, type, async () => {
      await this._ensureRepo();
      let change = await Change.create(this.repo, null, this.branchPrefix + branch, this.fetchOpts);

      let id = document.id;
      let file;
      while (id == null) {
        let candidateId = this._generateId();
        let candidateFile = await change.get(this._filenameFor(document.type, candidateId, isSchema), { allowCreate: true });
        if (!candidateFile.exists()) {
          id = candidateId;
          file = candidateFile;
        }
      }

      if (!file) {
        file = await change.get(this._filenameFor(document.type, id, isSchema), { allowCreate: true });
      }

      let gitDocument = { id, type: document.type };
      if (document.attributes) {
        gitDocument.attributes = document.attributes;
      }
      if (document.relationships) {
        gitDocument.relationships = document.relationships;
      }

      let pending = new PendingChange(null, gitDocument, finalizer.bind(this));
      let signature = await this._commitOptions('create', document.type, id, session);
      pendingChanges.set(pending, { type: document.type, id, signature, change, file });
      return pending;
    });
  }

  async prepareUpdate(branch, session, type, id, document, isSchema) {
    if (!document.meta || !document.meta.version) {
      throw new Error('missing required field "meta.version"', {
        status: 400,
        source: { pointer: '/data/meta/version' }
      });
    }

    await this._ensureRepo();
    return withErrorHandling(id, type, async () => {
      let change = await Change.create(this.repo, document.meta.version, this.branchPrefix + branch, this.fetchOpts);

      let file = await change.get(this._filenameFor(type, id, isSchema), { allowUpdate: true });
      let before = JSON.parse(await file.getBuffer());
      let after = patch(before, document);
      // we don't write id & type into the actual file (they're part
      // of the filename). But we want them present on the
      // PendingChange as complete valid documents.
      before.id = id;
      before.type = type;
      after.id = document.id;
      after.type = document.type;
      let signature = await this._commitOptions('update', type, id, session);
      let pending = new PendingChange(before, after, finalizer.bind(this));
      pendingChanges.set(pending, { type, id, signature, change, file });
      return pending;
    });
  }

  async prepareDelete(branch, session, version, type, id, isSchema) {
    if (!version) {
      throw new Error('version is required', {
        status: 400,
        source: { pointer: '/data/meta/version' }
      });
    }
    await this._ensureRepo();
    return withErrorHandling(id, type, async () => {
      let change = await Change.create(this.repo, version, this.branchPrefix + branch, this.fetchOpts);

      let file = await change.get(this._filenameFor(type, id, isSchema));
      let before = JSON.parse(await file.getBuffer());
      file.delete();
      before.id = id;
      before.type = type;
      let pending = new PendingChange(before, null, finalizer.bind(this));
      let signature = await this._commitOptions('delete', type, id, session);
      pendingChanges.set(pending, { type, id, signature, change });
      return pending;
    });
  }

  async _commitOptions(operation, type, id, session) {
    let user = session && await session.loadUser();
    let userAttributes = (user && user.data && user.data.attributes) || {};

    return {
      authorName: userAttributes['full-name'] || userAttributes.name || 'Anonymous Coward',
      authorEmail: userAttributes.email || 'anon@example.com',
      committerName: this.myName,
      committerEmail: this.myEmail,
      message: `${operation} ${type} ${String(id).slice(12)}`
    };
  }

  _filenameFor(type, id, isSchema) {
    let category = isSchema ? 'schema' : 'contents';
    let base = this.basePath ? this.basePath + '/' : '';
    return `${base}${category}/${type}/${id}.json`;
  }

  async _ensureRepo() {
    if (!this.repo) {
      if (this.remote) {
        let tempRepoPath = await mkdir('cardstack-temp-repo');
        this.repo = await Clone(this.remote.url, tempRepoPath, {
          fetchOpts: this.fetchOpts,
        });
        return;
      }

      this.repo = await Repository.open(this.repoPath);
    }
  }

  async _ensureGitchain() {
    await this._ensureRepo();

    if (!this.gitChain && this.hyperledgerConfig) {
      this.gitChain = new Gitchain(this.repo.path(), this.hyperledgerConfig);
    }
  }

  _generateId() {
    if (this.idGenerator) {
      return this.idGenerator();
    } else {
      // 20 bytes is good enough for git, so it's good enough for
      // me. In practice we probably have a lower collision
      // probability too, because we're allowed to retry if we know
      // the id is already in use (so we can really only collide
      // with things that have not yet merged into our branch).
      return crypto.randomBytes(20).toString('hex');
    }
  }

  async _pushToHyperledger() {
    await this._ensureGitchain();

    if(this.gitChain) {
      // make sure only one push is ongoing at a time, by creating a chain of
      // promises here
      this._gitChainPromise = Promise.resolve(this._gitChainPromise).then(() =>
        this.gitChain.push(this.hyperledgerConfig.tag)
      );
    }
  }
};

// TODO: we only need to do this here because the Hub has no generic
// "read" hook to call on writers. We should use that instead and move
// this into the generic hub:writers code.
function patch(before, diffDocument) {
  let after = Object.assign({}, before);
  for (let section of ['attributes', 'relationships']) {
    if (diffDocument[section]) {
      after[section] = Object.assign(
        {},
        before[section],
        diffDocument[section]
      );
    }
  }
  return after;
}

async function withErrorHandling(id, type, fn) {
  try {
    return await fn();
  } catch (err) {
    if (/Unable to parse OID/i.test(err.message) || /Object not found/i.test(err.message)) {
      throw new Error(err.message, { status: 400, source: { pointer: '/data/meta/version' }});
    }
    if (err instanceof Change.GitConflict) {
      throw new Error("Merge conflict", { status: 409 });
    }
    if (err instanceof Change.OverwriteRejected) {
      throw new Error(`id ${id} is already in use`, { status: 409, source: { pointer: '/data/id'}});
    }
    if (err instanceof Change.NotFound) {
      throw new Error(`${type} with id ${id} does not exist`, {
        status: 404,
        source: { pointer: '/data/id' }
      });
    }
    throw err;
  }
}

async function finalizer(pendingChange) {
  let { id, type, change, file, signature } = pendingChanges.get(pendingChange);
  return withErrorHandling(id, type, async () => {
    if (file) {
      if (pendingChange.finalDocument) {
        file.setContent(JSON.stringify({
          attributes: pendingChange.finalDocument.attributes,
          relationships: pendingChange.finalDocument.relationships
        }, null, 2));
      } else {
        file.delete();
      }
    }
    let version = await change.finalize(signature, this.remote, this.fetchOpts);
    await this._pushToHyperledger();
    return { version, hash: (file ? file.savedId() : null) };
  });
}
<commit_msg>Better error handling in branch merge<commit_after>const {
  Repository,
  Cred,
  Clone
} = require('nodegit');

const crypto = require('crypto');
const Change = require('./change');
const os = require('os');
const process = require('process');
const Error = require('@cardstack/plugin-utils/error');
const PendingChange = require('@cardstack/plugin-utils/pending-change');
const { promisify } = require('util');
const temp = require('temp').track();
const Gitchain = require('@cardstack/gitchain');
const log = require('@cardstack/logger')('cardstack/git');

const mkdir = promisify(temp.mkdir);

const pendingChanges = new WeakMap();

module.exports = class Writer {
  static create(params) {
    return new this(params);
  }
  constructor({ repo, idGenerator, basePath, branchPrefix, remote, hyperledger }) {
    this.repoPath = repo;
    this.basePath = basePath;
    this.branchPrefix = branchPrefix || "";
    this.repo = null;
    let hostname = os.hostname();
    this.myName = `PID${process.pid} on ${hostname}`;
    this.myEmail = `${os.userInfo().username}@${hostname}`;
    this.idGenerator = idGenerator;
    this.remote = remote;

    if (hyperledger) {
      let config = Object.assign({}, hyperledger);
      config.logger = log.info.bind(log); // TODO fix logger so we don't have to bind
      this.hyperledgerConfig = config;
    }

    if(remote) {
      this.fetchOpts = {
        callbacks: {
          credentials: (url, userName) => {
            if (remote && remote.privateKey) {
              return Cred.sshKeyMemoryNew(userName, remote.publicKey || '', remote.privateKey, remote.passphrase || '');
            }
            return Cred.sshKeyFromAgent(userName);
          }
        }
      };
    }
  }

  async prepareCreate(branch, session, type, document, isSchema) {
    return withErrorHandling(document.id, type, async () => {
      await this._ensureRepo();
      let change = await Change.create(this.repo, null, this.branchPrefix + branch, this.fetchOpts);

      let id = document.id;
      let file;
      while (id == null) {
        let candidateId = this._generateId();
        let candidateFile = await change.get(this._filenameFor(document.type, candidateId, isSchema), { allowCreate: true });
        if (!candidateFile.exists()) {
          id = candidateId;
          file = candidateFile;
        }
      }

      if (!file) {
        file = await change.get(this._filenameFor(document.type, id, isSchema), { allowCreate: true });
      }

      let gitDocument = { id, type: document.type };
      if (document.attributes) {
        gitDocument.attributes = document.attributes;
      }
      if (document.relationships) {
        gitDocument.relationships = document.relationships;
      }

      let pending = new PendingChange(null, gitDocument, finalizer.bind(this));
      let signature = await this._commitOptions('create', document.type, id, session);
      pendingChanges.set(pending, { type: document.type, id, signature, change, file });
      return pending;
    });
  }

  async prepareUpdate(branch, session, type, id, document, isSchema) {
    if (!document.meta || !document.meta.version) {
      throw new Error('missing required field "meta.version"', {
        status: 400,
        source: { pointer: '/data/meta/version' }
      });
    }

    await this._ensureRepo();
    return withErrorHandling(id, type, async () => {
      let change = await Change.create(this.repo, document.meta.version, this.branchPrefix + branch, this.fetchOpts);

      let file = await change.get(this._filenameFor(type, id, isSchema), { allowUpdate: true });
      let before = JSON.parse(await file.getBuffer());
      let after = patch(before, document);
      // we don't write id & type into the actual file (they're part
      // of the filename). But we want them present on the
      // PendingChange as complete valid documents.
      before.id = id;
      before.type = type;
      after.id = document.id;
      after.type = document.type;
      let signature = await this._commitOptions('update', type, id, session);
      let pending = new PendingChange(before, after, finalizer.bind(this));
      pendingChanges.set(pending, { type, id, signature, change, file });
      return pending;
    });
  }

  async prepareDelete(branch, session, version, type, id, isSchema) {
    if (!version) {
      throw new Error('version is required', {
        status: 400,
        source: { pointer: '/data/meta/version' }
      });
    }
    await this._ensureRepo();
    return withErrorHandling(id, type, async () => {
      let change = await Change.create(this.repo, version, this.branchPrefix + branch, this.fetchOpts);

      let file = await change.get(this._filenameFor(type, id, isSchema));
      let before = JSON.parse(await file.getBuffer());
      file.delete();
      before.id = id;
      before.type = type;
      let pending = new PendingChange(before, null, finalizer.bind(this));
      let signature = await this._commitOptions('delete', type, id, session);
      pendingChanges.set(pending, { type, id, signature, change });
      return pending;
    });
  }

  async _commitOptions(operation, type, id, session) {
    let user = session && await session.loadUser();
    let userAttributes = (user && user.data && user.data.attributes) || {};

    return {
      authorName: userAttributes['full-name'] || userAttributes.name || 'Anonymous Coward',
      authorEmail: userAttributes.email || 'anon@example.com',
      committerName: this.myName,
      committerEmail: this.myEmail,
      message: `${operation} ${type} ${String(id).slice(12)}`
    };
  }

  _filenameFor(type, id, isSchema) {
    let category = isSchema ? 'schema' : 'contents';
    let base = this.basePath ? this.basePath + '/' : '';
    return `${base}${category}/${type}/${id}.json`;
  }

  async _ensureRepo() {
    if (!this.repo) {
      if (this.remote) {
        let tempRepoPath = await mkdir('cardstack-temp-repo');
        this.repo = await Clone(this.remote.url, tempRepoPath, {
          fetchOpts: this.fetchOpts,
        });
        return;
      }

      this.repo = await Repository.open(this.repoPath);
    }
  }

  async _ensureGitchain() {
    await this._ensureRepo();

    if (!this.gitChain && this.hyperledgerConfig) {
      this.gitChain = new Gitchain(this.repo.path(), this.hyperledgerConfig);
    }
  }

  _generateId() {
    if (this.idGenerator) {
      return this.idGenerator();
    } else {
      // 20 bytes is good enough for git, so it's good enough for
      // me. In practice we probably have a lower collision
      // probability too, because we're allowed to retry if we know
      // the id is already in use (so we can really only collide
      // with things that have not yet merged into our branch).
      return crypto.randomBytes(20).toString('hex');
    }
  }

  async _pushToHyperledger() {
    await this._ensureGitchain();

    if(this.gitChain) {
      // make sure only one push is ongoing at a time, by creating a chain of
      // promises here
      this._gitChainPromise = Promise.resolve(this._gitChainPromise).then(() =>
        this.gitChain.push(this.hyperledgerConfig.tag).catch(e => {
          log.error("Error pushing to hyperledger:", e, e.stack);
        })
      );
    }
  }
};

// TODO: we only need to do this here because the Hub has no generic
// "read" hook to call on writers. We should use that instead and move
// this into the generic hub:writers code.
function patch(before, diffDocument) {
  let after = Object.assign({}, before);
  for (let section of ['attributes', 'relationships']) {
    if (diffDocument[section]) {
      after[section] = Object.assign(
        {},
        before[section],
        diffDocument[section]
      );
    }
  }
  return after;
}

async function withErrorHandling(id, type, fn) {
  try {
    return await fn();
  } catch (err) {
    if (/Unable to parse OID/i.test(err.message) || /Object not found/i.test(err.message)) {
      throw new Error(err.message, { status: 400, source: { pointer: '/data/meta/version' }});
    }
    if (err instanceof Change.GitConflict) {
      throw new Error("Merge conflict", { status: 409 });
    }
    if (err instanceof Change.OverwriteRejected) {
      throw new Error(`id ${id} is already in use`, { status: 409, source: { pointer: '/data/id'}});
    }
    if (err instanceof Change.NotFound) {
      throw new Error(`${type} with id ${id} does not exist`, {
        status: 404,
        source: { pointer: '/data/id' }
      });
    }
    throw err;
  }
}

async function finalizer(pendingChange) {
  let { id, type, change, file, signature } = pendingChanges.get(pendingChange);
  return withErrorHandling(id, type, async () => {
    if (file) {
      if (pendingChange.finalDocument) {
        file.setContent(JSON.stringify({
          attributes: pendingChange.finalDocument.attributes,
          relationships: pendingChange.finalDocument.relationships
        }, null, 2));
      } else {
        file.delete();
      }
    }
    let version = await change.finalize(signature, this.remote, this.fetchOpts);
    await this._pushToHyperledger();
    return { version, hash: (file ? file.savedId() : null) };
  });
}
<|endoftext|>