<commit_before>// Uses AMD or browser globals to create a module.

// Grabbed from https://github.com/umdjs/umd/blob/master/amdWeb.js.
// Check out https://github.com/umdjs/umd for more patterns.

// Defines a module "cart".
// Note that the name of the module is implied by the file name. It is best
// if the file name and the exported global have matching names.

// If you do not want to support the browser global path, then you
// can remove the `root` use and the passing `this` as the first arg to
// the top function.

(function (root, factory) {
  'use strict';
  if (typeof define === 'function' && define.amd) {
    define(['quickcart'], function (quickcart) {
      return factory(quickcart);
    });
  }
  else if (typeof module !== 'undefined' && module.exports && require) {
    module.exports = factory(require('../node_modules/quickcart/quickcart.js'));
  }
  else {
    root.QuickCart = factory(root.Cart);
  }
}(this, function (Cart) {
  'use strict';

  var DATA_ATTRIBUTE_PREFIX = 'data-quickcart-'
    , DATA_ATTRIBUTE_PREFIX_ITEM = DATA_ATTRIBUTE_PREFIX + 'item-'
    , DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY = DATA_ATTRIBUTE_PREFIX_ITEM + 'property-'
    , DEFAULT_TEMPLATES = {}
    , __uid = 0;

  function lazyCompileTemplate(type, source) {
    var compiled = null;
    DEFAULT_TEMPLATES[type] = {
      render: function() {
        compiled = compiled || Hogan.compile(source.join('\n'));
        return compiled.render.apply(compiled, arguments);
      }
    };
  }

  lazyCompileTemplate('item-row', [
    '
',
      '',
        '{{properties.name}}',
        '
',
        '<button ' + DATA_ATTRIBUTE_PREFIX + 'action="remove">Remove</button>',
      '	',
      '',
        '{{localizedPrice}}',
      '	',
      '',
        '<input type="number" value="{{quantity}}" ' + DATA_ATTRIBUTE_PREFIX + 'action="update" ' + DATA_ATTRIBUTE_PREFIX + 'commit="change" ' + DATA_ATTRIBUTE_PREFIX + 'target="quantity" data-quickcart-propertyvalue="javascript:parseInt(getValue(this), 10)">',
      '	',
      '',
        '{{localizedSubtotal}}',
      '	',
    '
',
  ]);

  function QuickCartView(doc, options) {
    options = options || {};
    this.doc = doc;
    this.localize = options.localizeAmount;
    this.handlers = options.handlers;
    this.renderer = options.renderer;
    this.objectReferences = {};
  }

  QuickCartView.prototype.getValue = function(element, ignorePropertyValue) {
    if (!ignorePropertyValue && element.getAttribute(DATA_ATTRIBUTE_PREFIX + 'propertyvalue')) {
      var dataValue = element.getAttribute(DATA_ATTRIBUTE_PREFIX + 'propertyvalue')
        , isJsThis = 0 === dataValue.indexOf('this.')
        , isJsProto = 0 === dataValue.indexOf('javascript:');
      if (isJsThis || isJsProto) {
        if (isJsProto) {
          dataValue = dataValue.substr('javascript:'.length);
        }
        return (function(getValue) {
          console.log('datavalue', element, dataValue);
          return eval(dataValue);
        }).call(element, function(element) {
          return QuickCartView.prototype.getValue(element, true);
        });
      }
      else {
        return dataValue;
      }
    }
    else if ('value' in element) {
      return element.value
    }
    else if ('selectedIndex' in element) {
      return element.options[element.selectedIndex].value;
    }
    else {
      return element.innerHTML;
    }
  };

  QuickCartView.prototype.getProperties = function(element, allProperties) {
    var attrs = {}
      , itemProperties = {}
      , prefix = allProperties ? DATA_ATTRIBUTE_PREFIX : DATA_ATTRIBUTE_PREFIX_ITEM;

    if (element.hasAttributes()) {
      for (var i=0; i < element.attributes.length; i++) {
        var attr = element.attributes[i]
          , k = attr.name
          , v = attr.value[0] === '$' ? this.getValue(this.doc.querySelector(attr.value.substr(1))) : attr.value;
        if (0 === k.indexOf(DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY)) {
          itemProperties[k.substr(DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY.length)] = v;
        }

        if (0 === k.indexOf(prefix)) {
          attrs[k.substr(prefix.length)] = v;
        }
      }
    }

    return {
        id: attrs.id
      , product: attrs.product
      , price: parseInt(attrs.price, 10)
      , quantity: parseInt(attrs.quantity, 10)
      , properties: itemProperties
    };
  };

  QuickCartView.prototype.setProperties = function(element, properties) {
    for (var k in properties) {
      element.setAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + k, properties[k]);
    }
  };

  QuickCartView.prototype.getItem = function(element) {
    console.log('getItem', element);
    if (element) {
      while (!element.getAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + 'id')) {
        element = element.parentNode;
        if (element === this.doc) {
          console.log('getItem found document. aborting.');
          return null;
        }
        console.log('\t-> parent', element);
      }
      return this.objectReferences[element.getAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + 'id')];
    }
    console.log('getItem failed');
    return null;
  };

  QuickCartView.prototype.init = function() {
    var _this = this;
    var handler = function() {
      this.handleEvent = function(event) {
        var cartAction = event.target.getAttribute('data-quickcart-action')
          , cartCommit = event.target.getAttribute('data-quickcart-commit');
        if (cartAction && (!cartCommit || cartCommit.trim().toLowerCase().split(/\s*\,\s*/g).indexOf(event.type) > -1)) {
          var cartTarget = event.target.getAttribute('data-quickcart-target');

          var properties = _this.getItem(event.target) || _this.getProperties(event.target);
          console.log('view:event', event.type, properties, event);

          if (cartTarget) {
            var val = _this.getValue(event.target);
            eval('properties.' + cartTarget + ' = val');
          }
          _this.handlers[cartAction](event.target, properties);
        }
      };
      _this.doc.addEventListener('click', this, false);
      _this.doc.addEventListener('change', this, false);
    };
    new handler();
  };

  QuickCartView.prototype.getContext = function(item) {
    var context = Object.create(item);
    context.localizedSubtotal = this.localize(context.subtotal);
    context.localizedPrice = this.localize(context.price);
    return context;
  };

  QuickCartView.prototype.add = function(item) {
    var listings = this.doc.querySelectorAll('.quickcart-items');
    var rendered = this.renderer('item-row', this.getContext(item));
    this.objectReferences[item.id] = item;
    for (var i=0; i < listings.length; i++) {
      var listing = listings[i];
      listing.innerHTML += rendered;
    }
  };

  QuickCartView.prototype.remove = function(item) {
    var items = this.doc.querySelectorAll('.quickcart-items [' + DATA_ATTRIBUTE_PREFIX_ITEM + 'id="' + item.id + '"]');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.parentNode.removeChild(element);
    }
    delete this.objectReferences[item.id];
  };

  QuickCartView.prototype.update = function(item) {
    var items = this.doc.querySelectorAll('.quickcart-items [' + DATA_ATTRIBUTE_PREFIX_ITEM + 'id="' + item.id + '"]');
    var rendered = this.renderer('item-row', this.getContext(item));
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.outerHTML = rendered;
    }
  };

  QuickCartView.prototype.count = function(count) {
    var items = this.doc.querySelectorAll('.quickcart-count');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.innerHTML = count;
    }
  };

  QuickCartView.prototype.total = function(total) {
    var items = this.doc.querySelectorAll('.quickcart-total');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.innerHTML = this.localize(total);
    }
  };

  var _uid = 0;
  function QuickCart(doc, options) {
    options = options || {};
    options.model = options.cart || {};
    var _this = this;
    this.options = options;
    this.renderer = this.options.renderer || function(type, context) {
      return DEFAULT_TEMPLATES[type].render(context);
    };
    this.doc = doc;
    this.view = null;
    this.model = new Cart(options.model);
    this.storage = options.storage || null;

    // TODO: implement something that polls storage for changes and syncs so that multiple tabs work as expected
  }

  QuickCart.prototype.add = function(product, quantity) {
    if (typeof product === 'string') {
      product = {
          product: product
        , quantity: quantity || 1
      };
    }
    product.id = QuickCart.generateItemUID();
    this.model.add(product);
    return product.id;
  };

  QuickCart.prototype.update = function(cartId, properties) {
    var item = this.model.find(cartId);
    for (var k in properties) {
      item[k] = properties[k];
    }
  };

  QuickCart.prototype.init = function() {
    this._initView();
    this._initModel();
  };

  QuickCart.prototype._initView = function() {
    var _this = this;
    this.view = new QuickCartView(this.doc, {
      localizeAmount: function(amt) {
        return '$' + (amt / 100).toFixed(2);
      },
      handlers: {
        add: function(element, properties) {
          if (properties.product && properties.price) {
            var cartId = properties.id || QuickCart.generateItemUID();
            properties.id = cartId;
            _this.model.add(properties);
          }
          else {
            console.warn('Product being added from view is incomplete', element, properties);
          }
        },
        remove: function(element, properties) {
          if (properties.id) {
            _this.model.remove(properties.id);
          }
          else {
            console.warn('Product being removed from view is incomplete', element, properties);
          }
        },
        update: function(element, properties) {
          if (properties.id) {
            var item = _this.model.find(properties.id);
            ['product','price','signature','quantity'].forEach(function(element) {
              if (item[element] !== properties[element]) {
                item[element] = properties[element];
              }
            });
            for (var propertyName in properties.properties) {
              item.property(propertyName, properties.properties[propertyName]);
            }
            _this.update(item);
          }
          else {
            console.warn('Product being updated from view is incomplete', element, properties);
          }
        },
        empty: function(element, properties) {
          _this.model.clear();
        },
      },
      renderer: this.renderer
    });
    this.view.init();
    this.view.count(0);
    this.view.total(0);
  };

  QuickCart.prototype._initModel = function() {
    var _this = this;

    this.model.on('item:add', function(item) {
      _this.view.add(item);
    });

    this.model.on('item:remove', function(item) {
      _this.view.remove(item);
    });

    this.model.on('item:change', function(item) {
      _this.view.update(item);
    });

    this.model.on('count', function() {
      _this.view.count(_this.model.count);
    });

    this.model.on('total', function() {
      _this.view.total(_this.model.total);
      _this.save();
    });

    var savedState = null;
    if (this.storage) {
      savedState = this.storage.load();
    }
    if (savedState && savedState.items) {
      savedState.items.forEach(function(item) {
        item.id = QuickCart.generateItemUID();
      });
      this.model.add(savedState.items);
    }
  };

  QuickCart.prototype.empty = function() {
    this.model.clear();
  };

  QuickCart.prototype.save = function() {
    if (this.storage) {
      this.storage.save(this.model);
    }
  };

  QuickCart.prototype.purchase = function(callback) {
    var _this = this;
    // TODO: import purchase code from cart model
    throw new Error('Not implemented');
  };

  QuickCart.generateItemUID = function() {
    __uid++;
    return 'quickcartitem' + __uid;
  };

  QuickCart.browserStorage = function(type, scope) {
    scope = scope || window;
    var engine = scope[type + 'Storage']
      , storageKey = 'quickcart';
    if (!engine) {
      throw new Error('Invalid browser storage engine: ' + type);
    }
    return {
      save: function(data) {
        var json = JSON.stringify(data, null, 2);
        console.debug('<- Saving state', json);
        engine.setItem(storageKey, json);
      },
      load: function() {
        var json = engine.getItem(storageKey)
          , parsed;
        console.debug('-> Loading state', json);
        if (json) {
          parsed = JSON.parse(json);
          for (var i=0; i < parsed.items.length; i++) {
            if (typeof parsed.items[i].price === 'string') { // expects price handlers to be in the provided scope
              parsed.items[i].price = scope[parsed.items[i].price];
            }
          }
        }
        else {
          return null;
        }
      }
    };
  };

  return QuickCart;
}));
<commit_msg>fix load saved<commit_after>// Uses AMD or browser globals to create a module.

// Grabbed from https://github.com/umdjs/umd/blob/master/amdWeb.js.
// Check out https://github.com/umdjs/umd for more patterns.

// Defines a module "cart".
// Note that the name of the module is implied by the file name. It is best
// if the file name and the exported global have matching names.

// If you do not want to support the browser global path, then you
// can remove the `root` use and the passing `this` as the first arg to
// the top function.

(function (root, factory) {
  'use strict';
  if (typeof define === 'function' && define.amd) {
    define(['quickcart'], function (quickcart) {
      return factory(quickcart);
    });
  }
  else if (typeof module !== 'undefined' && module.exports && require) {
    module.exports = factory(require('../node_modules/quickcart/quickcart.js'));
  }
  else {
    root.QuickCart = factory(root.Cart);
  }
}(this, function (Cart) {
  'use strict';

  var DATA_ATTRIBUTE_PREFIX = 'data-quickcart-'
    , DATA_ATTRIBUTE_PREFIX_ITEM = DATA_ATTRIBUTE_PREFIX + 'item-'
    , DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY = DATA_ATTRIBUTE_PREFIX_ITEM + 'property-'
    , DEFAULT_TEMPLATES = {}
    , __uid = 0;

  function lazyCompileTemplate(type, source) {
    var compiled = null;
    DEFAULT_TEMPLATES[type] = {
      render: function() {
        compiled = compiled || Hogan.compile(source.join('\n'));
        return compiled.render.apply(compiled, arguments);
      }
    };
  }

  lazyCompileTemplate('item-row', [
    '
',
      '',
        '{{properties.name}}',
        '
',
        '<button ' + DATA_ATTRIBUTE_PREFIX + 'action="remove">Remove</button>',
      '	',
      '',
        '{{localizedPrice}}',
      '	',
      '',
        '<input type="number" value="{{quantity}}" ' + DATA_ATTRIBUTE_PREFIX + 'action="update" ' + DATA_ATTRIBUTE_PREFIX + 'commit="change" ' + DATA_ATTRIBUTE_PREFIX + 'target="quantity" data-quickcart-propertyvalue="javascript:parseInt(getValue(this), 10)">',
      '	',
      '',
        '{{localizedSubtotal}}',
      '	',
    '
',
  ]);

  function QuickCartView(doc, options) {
    options = options || {};
    this.doc = doc;
    this.localize = options.localizeAmount;
    this.handlers = options.handlers;
    this.renderer = options.renderer;
    this.objectReferences = {};
  }

  QuickCartView.prototype.getValue = function(element, ignorePropertyValue) {
    if (!ignorePropertyValue && element.getAttribute(DATA_ATTRIBUTE_PREFIX + 'propertyvalue')) {
      var dataValue = element.getAttribute(DATA_ATTRIBUTE_PREFIX + 'propertyvalue')
        , isJsThis = 0 === dataValue.indexOf('this.')
        , isJsProto = 0 === dataValue.indexOf('javascript:');
      if (isJsThis || isJsProto) {
        if (isJsProto) {
          dataValue = dataValue.substr('javascript:'.length);
        }
        return (function(getValue) {
          console.log('datavalue', element, dataValue);
          return eval(dataValue);
        }).call(element, function(element) {
          return QuickCartView.prototype.getValue(element, true);
        });
      }
      else {
        return dataValue;
      }
    }
    else if ('value' in element) {
      return element.value
    }
    else if ('selectedIndex' in element) {
      return element.options[element.selectedIndex].value;
    }
    else {
      return element.innerHTML;
    }
  };

  QuickCartView.prototype.getProperties = function(element, allProperties) {
    var attrs = {}
      , itemProperties = {}
      , prefix = allProperties ? DATA_ATTRIBUTE_PREFIX : DATA_ATTRIBUTE_PREFIX_ITEM;

    if (element.hasAttributes()) {
      for (var i=0; i < element.attributes.length; i++) {
        var attr = element.attributes[i]
          , k = attr.name
          , v = attr.value[0] === '$' ? this.getValue(this.doc.querySelector(attr.value.substr(1))) : attr.value;
        if (0 === k.indexOf(DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY)) {
          itemProperties[k.substr(DATA_ATTRIBUTE_PREFIX_ITEM_PROPERTY.length)] = v;
        }

        if (0 === k.indexOf(prefix)) {
          attrs[k.substr(prefix.length)] = v;
        }
      }
    }

    return {
        id: attrs.id
      , product: attrs.product
      , price: parseInt(attrs.price, 10)
      , quantity: parseInt(attrs.quantity, 10)
      , properties: itemProperties
    };
  };

  QuickCartView.prototype.setProperties = function(element, properties) {
    for (var k in properties) {
      element.setAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + k, properties[k]);
    }
  };

  QuickCartView.prototype.getItem = function(element) {
    console.log('getItem', element);
    if (element) {
      while (!element.getAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + 'id')) {
        element = element.parentNode;
        if (element === this.doc) {
          console.log('getItem found document. aborting.');
          return null;
        }
        console.log('\t-> parent', element);
      }
      return this.objectReferences[element.getAttribute(DATA_ATTRIBUTE_PREFIX_ITEM + 'id')];
    }
    console.log('getItem failed');
    return null;
  };

  QuickCartView.prototype.init = function() {
    var _this = this;
    var handler = function() {
      this.handleEvent = function(event) {
        var cartAction = event.target.getAttribute('data-quickcart-action')
          , cartCommit = event.target.getAttribute('data-quickcart-commit');
        if (cartAction && (!cartCommit || cartCommit.trim().toLowerCase().split(/\s*\,\s*/g).indexOf(event.type) > -1)) {
          var cartTarget = event.target.getAttribute('data-quickcart-target');

          var properties = _this.getItem(event.target) || _this.getProperties(event.target);
          console.log('view:event', event.type, properties, event);

          if (cartTarget) {
            var val = _this.getValue(event.target);
            eval('properties.' + cartTarget + ' = val');
          }
          _this.handlers[cartAction](event.target, properties);
        }
      };
      _this.doc.addEventListener('click', this, false);
      _this.doc.addEventListener('change', this, false);
    };
    new handler();
  };

  QuickCartView.prototype.getContext = function(item) {
    var context = Object.create(item);
    context.localizedSubtotal = this.localize(context.subtotal);
    context.localizedPrice = this.localize(context.price);
    return context;
  };

  QuickCartView.prototype.add = function(item) {
    var listings = this.doc.querySelectorAll('.quickcart-items');
    var rendered = this.renderer('item-row', this.getContext(item));
    this.objectReferences[item.id] = item;
    for (var i=0; i < listings.length; i++) {
      var listing = listings[i];
      listing.innerHTML += rendered;
    }
  };

  QuickCartView.prototype.remove = function(item) {
    var items = this.doc.querySelectorAll('.quickcart-items [' + DATA_ATTRIBUTE_PREFIX_ITEM + 'id="' + item.id + '"]');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.parentNode.removeChild(element);
    }
    delete this.objectReferences[item.id];
  };

  QuickCartView.prototype.update = function(item) {
    var items = this.doc.querySelectorAll('.quickcart-items [' + DATA_ATTRIBUTE_PREFIX_ITEM + 'id="' + item.id + '"]');
    var rendered = this.renderer('item-row', this.getContext(item));
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.outerHTML = rendered;
    }
  };

  QuickCartView.prototype.count = function(count) {
    var items = this.doc.querySelectorAll('.quickcart-count');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.innerHTML = count;
    }
  };

  QuickCartView.prototype.total = function(total) {
    var items = this.doc.querySelectorAll('.quickcart-total');
    for (var i=0; i < items.length; i++) {
      var element = items[i];
      element.innerHTML = this.localize(total);
    }
  };

  var _uid = 0;
  function QuickCart(doc, options) {
    options = options || {};
    options.model = options.cart || {};
    var _this = this;
    this.options = options;
    this.renderer = this.options.renderer || function(type, context) {
      return DEFAULT_TEMPLATES[type].render(context);
    };
    this.doc = doc;
    this.view = null;
    this.model = new Cart(options.model);
    this.storage = options.storage || null;

    // TODO: implement something that polls storage for changes and syncs so that multiple tabs work as expected
  }

  QuickCart.prototype.add = function(product, quantity) {
    if (typeof product === 'string') {
      product = {
          product: product
        , quantity: quantity || 1
      };
    }
    product.id = QuickCart.generateItemUID();
    this.model.add(product);
    return product.id;
  };

  QuickCart.prototype.update = function(cartId, properties) {
    var item = this.model.find(cartId);
    for (var k in properties) {
      item[k] = properties[k];
    }
  };

  QuickCart.prototype.init = function() {
    this._initView();
    this._initModel();
  };

  QuickCart.prototype._initView = function() {
    var _this = this;
    this.view = new QuickCartView(this.doc, {
      localizeAmount: function(amt) {
        return '$' + (amt / 100).toFixed(2);
      },
      handlers: {
        add: function(element, properties) {
          if (properties.product && properties.price) {
            var cartId = properties.id || QuickCart.generateItemUID();
            properties.id = cartId;
            _this.model.add(properties);
          }
          else {
            console.warn('Product being added from view is incomplete', element, properties);
          }
        },
        remove: function(element, properties) {
          if (properties.id) {
            _this.model.remove(properties.id);
          }
          else {
            console.warn('Product being removed from view is incomplete', element, properties);
          }
        },
        update: function(element, properties) {
          if (properties.id) {
            var item = _this.model.find(properties.id);
            ['product','price','signature','quantity'].forEach(function(element) {
              if (item[element] !== properties[element]) {
                item[element] = properties[element];
              }
            });
            for (var propertyName in properties.properties) {
              item.property(propertyName, properties.properties[propertyName]);
            }
            _this.update(item);
          }
          else {
            console.warn('Product being updated from view is incomplete', element, properties);
          }
        },
        empty: function(element, properties) {
          _this.model.clear();
        },
      },
      renderer: this.renderer
    });
    this.view.init();
    this.view.count(0);
    this.view.total(0);
  };

  QuickCart.prototype._initModel = function() {
    var _this = this;

    this.model.on('item:add', function(item) {
      _this.view.add(item);
    });

    this.model.on('item:remove', function(item) {
      _this.view.remove(item);
    });

    this.model.on('item:change', function(item) {
      _this.view.update(item);
    });

    this.model.on('count', function() {
      _this.view.count(_this.model.count);
    });

    this.model.on('total', function() {
      _this.view.total(_this.model.total);
      _this.save();
    });

    var savedState = null;
    if (this.storage) {
      savedState = this.storage.load();
    }
    if (savedState && savedState.items) {
      savedState.items.forEach(function(item) {
        item.id = QuickCart.generateItemUID();
      });
      this.model.add(savedState.items);
    }
  };

  QuickCart.prototype.empty = function() {
    this.model.clear();
  };

  QuickCart.prototype.save = function() {
    if (this.storage) {
      this.storage.save(this.model);
    }
  };

  QuickCart.prototype.purchase = function(callback) {
    var _this = this;
    // TODO: import purchase code from cart model
    throw new Error('Not implemented');
  };

  QuickCart.generateItemUID = function() {
    __uid++;
    return 'quickcartitem' + __uid;
  };

  QuickCart.browserStorage = function(type, scope) {
    scope = scope || window;
    var engine = scope[type + 'Storage']
      , storageKey = 'quickcart';
    if (!engine) {
      throw new Error('Invalid browser storage engine: ' + type);
    }
    return {
      save: function(data) {
        var json = JSON.stringify(data, null, 2);
        console.debug('<- Saving state', json);
        engine.setItem(storageKey, json);
      },
      load: function() {
        var json = engine.getItem(storageKey)
          , parsed = null;
        console.debug('-> Loading state', json);
        if (json) {
          parsed = JSON.parse(json);
          for (var i=0; i < parsed.items.length; i++) {
            if (typeof parsed.items[i].price === 'string') { // expects price handlers to be in the provided scope
              parsed.items[i].price = scope[parsed.items[i].price];
            }
          }
        }
        return parsed;
      }
    };
  };

  return QuickCart;
}));
<|endoftext|>


<commit_before>const Resp = require('respjs');
const EventEmitter = require('events');

/**
 * Class to handle a redis connection and provide events to react on for
 * all incoming Redis commands
 */
class RedisHandler extends EventEmitter {
    /**
     * Initialize and register all data handlers to send out events on new commands
     * @param socket Network Socket/Connection
     * @param options options objects, currently mainly for logger
     */
    constructor(socket, options) {
        super();

        options = options || {};
        this.log = options.log || console;
        this.logScope = options.logScope || '';
        if (this.logScope.length) this.logScope += ' ';
        this.socket = socket;

        this.sockedId = this.logScope + socket.remoteAddress + ':' + socket.remotePort;
        this.initialized = false;

        this.writeQueue = [];

        this.handleBuffers = false;
        const respOptions = {};
        if (options.handleAsBuffers) {
            this.handleBuffers = true;
            respOptions.bufBulk = true;
        }
        this.resp = new Resp(respOptions);

        this.resp.on('error', (err) => {
            this.log.error(this.sockedId + ' (Init=' + this.initialized + ') Redis error:' + err);
            if (this.initialized) {
                this.sendError(null,new Error('PARSER ERROR ' + err)); // TODO
            }
            else {
                this.close();
            }
        });

        this.resp.on('data', (data) => this._handleCommand(data));
        this.resp.on('finish', () => {
            this.log.debug(this.sockedId + ' Redis finish');
        });

        socket.on('data', (data) => {
            this.log.silly(this.sockedId + ' New Redis request: ' + ((data.length > 1024) ? data.toString().replace(/[\r\n]+/g, '').substring(0,100) + ' -- ' + data.length + ' bytes' : data.toString().replace(/[\r\n]+/g, ''))); // TODO remove data logging (performance)
            this.resp.write(data);
        });

        socket.on('error', (err) => {
            this.log.error(this.sockedId + ' Redis error: ' + err);
            this.socket.destroy();
        });
    }

    /**
     * Handle one incoming command, assign responseId and emit event to be handled
     * @param data Array RESP data
     * @private
     */
    _handleCommand(data) {
        let command = data.splice(0, 1)[0];
        if (this.handleBuffers) {
            // Command and pot. first parameter should always be Buffer
            command = command.toString('utf-8');
            if (Buffer.isBuffer(data[0])) data[0] = data[0].toString('utf-8');
            // Binary data only relevant for GET and SET in our case
            if (command !== 'set' && data.length > 1) {
                for (let i = 1; i < data.length; i++) {
                    if (Buffer.isBuffer(data[i])) data[i] = data[i].toString('utf-8');
                }
            }
        }
        if (command === 'info') this.initialized = true;
        const t = process.hrtime();
        const responseId = (t[0] * 1e3) + (t[1] / 1e6);
        this.log.silly(this.sockedId + ' Parser result: id=' + responseId + ', command=' + command + ', data=' + ((JSON.stringify(data).length > 1024) ? JSON.stringify(data).substring(0,100) + ' -- ' + JSON.stringify(data).length + ' bytes' : JSON.stringify(data))); // TODO remove data logging (performance)
        this.writeQueue.push({id: responseId, data: false});
        if (this.listenerCount(command) !== 0) {
            this.emit(command, data, responseId);
        }
        else {
            this.sendError(responseId, new Error(command + ' NOT SUPPORTED'));
        }
    }

    /**
     * Check if the response to a certain command can be send out directly or
     * if it needs to wait till earlier responses are ready
     * @param responseId ID of the response
     * @param data Buffer to send out
     * @private
     */
    _sendQueued(responseId, data) {
        let idx = 0;
        while (this.writeQueue.length && idx < this.writeQueue.length) {
            // we found the queue entry that matches with the responseId, so store the data so be sent out
            if (this.writeQueue[idx].id === responseId) {
                this.writeQueue[idx].data = data;
                // if we found it not on first index we are done because we can not send it out,
                // need to wait for index 0 to have a complete answer
                if (idx > 0) break;
            }
            // when data for queue entry 0 are preset (!== false) we can send it, remove the first entry
            // and check the other entries if they have completed responses too
            if (idx === 0 && this.writeQueue[idx].data !== false) {
                const response = this.writeQueue.shift();
                this.log.silly(this.sockedId + ' Redis response (' + response.id + '): ' + ((response.data.length > 1024) ? data.length + ' bytes' : response.data.toString().replace(/[\r\n]+/g, ''))); // TODO remove data logging (performance)
                this._write(response.data);
                // We sended out first queue entry but no further response is ready
                // and we do not need to check the whole queue, so we are done here
                if (this.writeQueue.length && this.writeQueue[idx].data === false) break;
            }
            // we have not found the response on the current index, try next one
            else {
                idx++;
            }
        }

        if (idx > 0) {
            this.log.silly(this.sockedId + ' Redis response (' + responseId + '): Response queued');
        }
    }

    /**
     * Really write out a response to the network connection
     * @param data Buffer to send out
     * @private
     */
    _write(data) {
        this.socket.write(data);
    }

    /**
     * Guard to make sure a response is valid to be sent out
     * @param responseId ID of the response
     * @param data Buffer to send out
     */
    sendResponse(responseId, data) {
        // handle responses without a specific request like publishing data, so send directly
        if (responseId === null) {
            this.log.silly(this.sockedId + ' Redis response DIRECT: ' + data.toString().replace(/[\r\n]+/g, '')); // TODO remove data logging (performance)
            return this._write(data);
        }
        if (!responseId) {
            throw Error('Invalid implementation: no responseId provided!');
        }
        if (!data) {
            this.log.error('Not able to write ' + JSON.stringify(data));
            data = Resp.encodeError(new Error('INVALID RESPONSE: ' + JSON.stringify(data)));
        }
        setImmediate(() => this._sendQueued(responseId, data));
    }

    /**
     * Close network connection
     */
    close() {
        this.log.silly(this.sockedId + ' Redis connection close');
        this.socket.end();
    }

    /**
     * Encode RESP's Null value to RESP buffer and send out
     * @param responseId ID of the response
     */
    sendNull(responseId) {
        this.sendResponse(responseId, Resp.encodeNull());
    }

    /**
     * Encode RESP's Null Array value to RESP buffer and send out
     * @param responseId ID of the response
     */
    sendNullArray(responseId) {
        this.sendResponse(responseId, Resp.encodeNullArray());
    }

    /**
     * Encode string to RESP buffer and send out
     * @param responseId ID od the response
     * @param str String to encode
     */
    sendString(responseId, str) {
        this.sendResponse(responseId, Resp.encodeString(str));
    }

    /**
     * Encode error object to RESP buffer and send out
     * @param responseId ID of the response
     * @param error Error object with error details to send out
     */
    sendError(responseId, error) {
        this.sendResponse(responseId, Resp.encodeError(error));
    }

    /**
     * Encode integer to RESP buffer and send out
     * @param responseId ID of the response
     * @param num Integer to send out
     */
    sendInteger(responseId, num) {
        this.sendResponse(responseId, Resp.encodeInteger(num));
    }

    /**
     * Encode RESP's bulk string to RESP buffer and send out
     * @param responseId ID of the response
     * @param str String to send out
     */
    sendBulk(responseId, str) {
        this.sendResponse(responseId, Resp.encodeBulk(str));
    }

    /**
     * Encode RESP's bulk buffer to RESP buffer.
     * @param responseId ID of the response
     * @param buf Buffer to send out
     */
    sendBufBulk(responseId, buf) {
        this.sendResponse(responseId, Resp.encodeBufBulk(buf));
    }

    /**
     * Encode an Array depending on the type of the elements
     * @param arr Array to encode
     * @returns {Array} Array with Buffers with encoded values
     */
    encodeRespArray(arr) {
        for (let i = 0; i < arr.length; i++) {
            if (Array.isArray(arr[i])) {
                arr[i] = this.encodeRespArray(arr[i]);
            }
            else if (Buffer.isBuffer(arr[i])) {
                arr[i] = Resp.encodeBufBulk(arr[i]);
            }
            else if (arr[i] === null) {
                arr[i] = Resp.encodeNull();
            }
            else if (typeof arr[i] === 'number') {
                arr[i] = Resp.encodeInteger(arr[i]);
            }
            else {
                arr[i] = Resp.encodeBulk(arr[i]);
            }
        }
        return arr;
    }

    /**
     * Encode a array values to buffers and send out
     * @param responseId ID of the response
     * @param arr Array to send out
     */
    sendArray(responseId, arr) {
        this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr)));
    }
}

module.exports = RedisHandler;
<commit_msg>optimize error logging<commit_after>const Resp = require('respjs');
const EventEmitter = require('events');

/**
 * Class to handle a redis connection and provide events to react on for
 * all incoming Redis commands
 */
class RedisHandler extends EventEmitter {
    /**
     * Initialize and register all data handlers to send out events on new commands
     * @param socket Network Socket/Connection
     * @param options options objects, currently mainly for logger
     */
    constructor(socket, options) {
        super();

        options = options || {};
        this.log = options.log || console;
        this.logScope = options.logScope || '';
        if (this.logScope.length) this.logScope += ' ';
        this.socket = socket;

        this.sockedId = this.logScope + socket.remoteAddress + ':' + socket.remotePort;
        this.initialized = false;
        this.stop = false;

        this.writeQueue = [];

        this.handleBuffers = false;
        const respOptions = {};
        if (options.handleAsBuffers) {
            this.handleBuffers = true;
            respOptions.bufBulk = true;
        }
        this.resp = new Resp(respOptions);

        this.resp.on('error', (err) => {
            this.log.error(this.sockedId + ' (Init=' + this.initialized + ') Redis error:' + err);
            if (this.initialized) {
                this.sendError(null,new Error('PARSER ERROR ' + err)); // TODO
            }
            else {
                this.close();
            }
        });

        this.resp.on('data', (data) => this._handleCommand(data));
        this.resp.on('finish', () => {
            this.log.debug(this.sockedId + ' Redis finish');
        });

        socket.on('data', (data) => {
            this.log.silly(this.sockedId + ' New Redis request: ' + ((data.length > 1024) ? data.toString().replace(/[\r\n]+/g, '').substring(0,100) + ' -- ' + data.length + ' bytes' : data.toString().replace(/[\r\n]+/g, ''))); // TODO remove data logging (performance)
            this.resp.write(data);
        });

        socket.on('error', (err) => {
            if (!this.stop) {
                this.log.error(this.sockedId + ' Redis error: ' + err);
            }
            if (this.socket) {
                this.socket.destroy();
            }
        });
    }

    /**
     * Handle one incoming command, assign responseId and emit event to be handled
     * @param data Array RESP data
     * @private
     */
    _handleCommand(data) {
        let command = data.splice(0, 1)[0];
        if (this.handleBuffers) {
            // Command and pot. first parameter should always be Buffer
            command = command.toString('utf-8');
            if (Buffer.isBuffer(data[0])) data[0] = data[0].toString('utf-8');
            // Binary data only relevant for GET and SET in our case
            if (command !== 'set' && data.length > 1) {
                for (let i = 1; i < data.length; i++) {
                    if (Buffer.isBuffer(data[i])) data[i] = data[i].toString('utf-8');
                }
            }
        }
        if (command === 'info') this.initialized = true;
        const t = process.hrtime();
        const responseId = (t[0] * 1e3) + (t[1] / 1e6);
        this.log.silly(this.sockedId + ' Parser result: id=' + responseId + ', command=' + command + ', data=' + ((JSON.stringify(data).length > 1024) ? JSON.stringify(data).substring(0,100) + ' -- ' + JSON.stringify(data).length + ' bytes' : JSON.stringify(data))); // TODO remove data logging (performance)
        this.writeQueue.push({id: responseId, data: false});
        if (this.listenerCount(command) !== 0) {
            this.emit(command, data, responseId);
        }
        else {
            this.sendError(responseId, new Error(command + ' NOT SUPPORTED'));
        }
    }

    /**
     * Check if the response to a certain command can be send out directly or
     * if it needs to wait till earlier responses are ready
     * @param responseId ID of the response
     * @param data Buffer to send out
     * @private
     */
    _sendQueued(responseId, data) {
        let idx = 0;
        while (this.writeQueue.length && idx < this.writeQueue.length) {
            // we found the queue entry that matches with the responseId, so store the data so be sent out
            if (this.writeQueue[idx].id === responseId) {
                this.writeQueue[idx].data = data;
                // if we found it not on first index we are done because we can not send it out,
                // need to wait for index 0 to have a complete answer
                if (idx > 0) break;
            }
            // when data for queue entry 0 are preset (!== false) we can send it, remove the first entry
            // and check the other entries if they have completed responses too
            if (idx === 0 && this.writeQueue[idx].data !== false) {
                const response = this.writeQueue.shift();
                this.log.silly(this.sockedId + ' Redis response (' + response.id + '): ' + ((response.data.length > 1024) ? data.length + ' bytes' : response.data.toString().replace(/[\r\n]+/g, ''))); // TODO remove data logging (performance)
                this._write(response.data);
                // We sended out first queue entry but no further response is ready
                // and we do not need to check the whole queue, so we are done here
                if (this.writeQueue.length && this.writeQueue[idx].data === false) break;
            }
            // we have not found the response on the current index, try next one
            else {
                idx++;
            }
        }

        if (idx > 0) {
            this.log.silly(this.sockedId + ' Redis response (' + responseId + '): Response queued');
        }
    }

    /**
     * Really write out a response to the network connection
     * @param data Buffer to send out
     * @private
     */
    _write(data) {
        this.socket.write(data);
    }

    /**
     * Guard to make sure a response is valid to be sent out
     * @param responseId ID of the response
     * @param data Buffer to send out
     */
    sendResponse(responseId, data) {
        // handle responses without a specific request like publishing data, so send directly
        if (responseId === null) {
            this.log.silly(this.sockedId + ' Redis response DIRECT: ' + data.toString().replace(/[\r\n]+/g, '')); // TODO remove data logging (performance)
            return this._write(data);
        }
        if (!responseId) {
            throw Error('Invalid implementation: no responseId provided!');
        }
        if (!data) {
            this.log.error('Not able to write ' + JSON.stringify(data));
            data = Resp.encodeError(new Error('INVALID RESPONSE: ' + JSON.stringify(data)));
        }
        setImmediate(() => this._sendQueued(responseId, data));
    }

    /**
     * Close network connection
     */
    close() {
        this.log.silly(this.sockedId + ' Redis connection close');
        this.stop = true;
        this.socket.end();
    }

    /**
     * Return if socket/handler active or closed
     * @returns {boolean} is Handler/Connection active (not closed)
     */
    isActive() {
        return !this.stop;
    }

    /**
     * Encode RESP's Null value to RESP buffer and send out
     * @param responseId ID of the response
     */
    sendNull(responseId) {
        this.sendResponse(responseId, Resp.encodeNull());
    }

    /**
     * Encode RESP's Null Array value to RESP buffer and send out
     * @param responseId ID of the response
     */
    sendNullArray(responseId) {
        this.sendResponse(responseId, Resp.encodeNullArray());
    }

    /**
     * Encode string to RESP buffer and send out
     * @param responseId ID od the response
     * @param str String to encode
     */
    sendString(responseId, str) {
        this.sendResponse(responseId, Resp.encodeString(str));
    }

    /**
     * Encode error object to RESP buffer and send out
     * @param responseId ID of the response
     * @param error Error object with error details to send out
     */
    sendError(responseId, error) {
        this.sendResponse(responseId, Resp.encodeError(error));
    }

    /**
     * Encode integer to RESP buffer and send out
     * @param responseId ID of the response
     * @param num Integer to send out
     */
    sendInteger(responseId, num) {
        this.sendResponse(responseId, Resp.encodeInteger(num));
    }

    /**
     * Encode RESP's bulk string to RESP buffer and send out
     * @param responseId ID of the response
     * @param str String to send out
     */
    sendBulk(responseId, str) {
        this.sendResponse(responseId, Resp.encodeBulk(str));
    }

    /**
     * Encode RESP's bulk buffer to RESP buffer.
     * @param responseId ID of the response
     * @param buf Buffer to send out
     */
    sendBufBulk(responseId, buf) {
        this.sendResponse(responseId, Resp.encodeBufBulk(buf));
    }

    /**
     * Encode an Array depending on the type of the elements
     * @param arr Array to encode
     * @returns {Array} Array with Buffers with encoded values
     */
    encodeRespArray(arr) {
        for (let i = 0; i < arr.length; i++) {
            if (Array.isArray(arr[i])) {
                arr[i] = this.encodeRespArray(arr[i]);
            }
            else if (Buffer.isBuffer(arr[i])) {
                arr[i] = Resp.encodeBufBulk(arr[i]);
            }
            else if (arr[i] === null) {
                arr[i] = Resp.encodeNull();
            }
            else if (typeof arr[i] === 'number') {
                arr[i] = Resp.encodeInteger(arr[i]);
            }
            else {
                arr[i] = Resp.encodeBulk(arr[i]);
            }
        }
        return arr;
    }

    /**
     * Encode a array values to buffers and send out
     * @param responseId ID of the response
     * @param arr Array to send out
     */
    sendArray(responseId, arr) {
        this.sendResponse(responseId, Resp.encodeArray(this.encodeRespArray(arr)));
    }
}

module.exports = RedisHandler;
<|endoftext|>


<commit_before>const dateFormat = require('dateformat');
const exif = require("jpeg-exif");
const n2f = require('num2fraction');
const path = require('path');

const replacements = {
  'i': {
    name: 'Index',
    description: 'The index of the file when renaming multiple files',
    parameters: {
      description: 'starting index, default is 1',
      default: '1'
    },
    unique: true,
    function: function(fileObj, args) {
      let newIndex = parseInt(args) - 1 + parseInt(fileObj.index);
      let totalFiles = parseInt(args) - 1 + parseInt(fileObj.totalFiles);
      return fileIndexString(totalFiles, newIndex);
    }
  },
  'f': {
    name: 'File name',
    description: "The original name of the file",
    parameters: {
      description: 'upper, lower, camel, pascal, or blank',
      default: ''
    },
    unique: true,
    function: function(fileObj, args) {
      switch (args) {
        case 'upper':
          return fileObj.name.toUpperCase();
        case 'lower':
          return fileObj.name.toLowerCase();
        case 'camel':
          return fileObj.name.toLowerCase().replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
            return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
          }).replace(/[\s\-_\.]+/g, '');
        case 'pascal':
          return fileObj.name.toLowerCase().replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter) {
            return letter.toUpperCase();
          }).replace(/[\s\-_\.]+/g, '');
        default:
          return fileObj.name;
      }
    }
  },
  'r': {
    name: 'RegEx',
    description: 'The match of the RegEx pattern specified in -r "..."',
    unique: false,
    function: function(fileObj) {
      if (fileObj.regexMatch) {
        return fileObj.regexMatch[0];
      } else {
        return '';
      }
    }
  },
  'p': {
    name: 'Parent directory',
    description: "The name of the parent directory",
    unique: false,
    function: function(fileObj) {
      return path.basename(fileObj.dir);
    }
  },
  'd': {
    name: 'Date',
    description: "The current date/time",
    parameters: {
      description: 'date format, default is yyyymmdd',
      default: 'yyyymmdd'
    },
    unique: false,
    function: function(fileObj, args) {
      let d = new Date();
      return dateFormat(d, args);
    }
  },
  'g': {
    name: 'GUID',
    description: "A globally unique identifier",
    unique: true,
    function: function() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
      });
    }
  },
  'eiso': {
    name: 'Exif ISO',
    description: "Photo ISO value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.SubExif && data.SubExif.PhotographicSensitivity ? data.SubExif.PhotographicSensitivity : '');
    }
  },
  'efnum': {
    name: 'Exif FNumber',
    description: "Photo FNumber value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.SubExif && data.SubExif.FNumber ? data.SubExif.FNumber[0] : '');
    }
  },
  'eex': {
    name: 'Exif Exposure Time',
    description: "Photo exposure time value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      if (typeof(data) === 'object' && data.SubExif && data.SubExif.ExposureTime) {
        if (data.SubExif.ExposureTime[0] < 1) {
          return n2f(data.SubExif.ExposureTime[0]);
        } else {
          return data.SubExif.ExposureTime[0];
        }
      } else {
        return '';
      }
    }
  },
  'ey': {
    name: 'Exif Year',
    description: "Year the photo was taken",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.DateTime ? data.DateTime.split(/:|\s/)[0] : '');
    }
  },
  'em': {
    name: 'Exif Month',
    description: "Month the photo was taken",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.DateTime ? data.DateTime.split(/:|\s/)[1] : '');
    }
  },
  'ed': {
    name: 'Exif Day',
    description: "Day the photo was taken",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.DateTime ? data.DateTime.split(/:|\s/)[2] : '');
    }
  }
};

function getExifData(file) {
  try {
    let data = exif.parseSync(file);
    return data;
  } catch (ex) {
    return '';
  }
}

function fileIndexString(total, index) { // append correct number of zeroes depending on total number of files
  let totString = '' + total;
  let returnString = '' + index;
  while (returnString.length < totString.length) {
    returnString = '0' + returnString;
  }
  return returnString;
}

module.exports = replacements;<commit_msg>Exif date/time formatting<commit_after>const dateFormat = require('dateformat');
const exif = require("jpeg-exif");
const n2f = require('num2fraction');
const path = require('path');

const replacements = {
  'i': {
    name: 'Index',
    description: 'The index of the file when renaming multiple files',
    parameters: {
      description: 'starting index, default is 1',
      default: '1'
    },
    unique: true,
    function: function(fileObj, args) {
      let newIndex = parseInt(args) - 1 + parseInt(fileObj.index);
      let totalFiles = parseInt(args) - 1 + parseInt(fileObj.totalFiles);
      return fileIndexString(totalFiles, newIndex);
    }
  },
  'f': {
    name: 'File name',
    description: "The original name of the file",
    parameters: {
      description: 'upper, lower, camel, pascal, or blank',
      default: ''
    },
    unique: true,
    function: function(fileObj, args) {
      switch (args) {
        case 'upper':
          return fileObj.name.toUpperCase();
        case 'lower':
          return fileObj.name.toLowerCase();
        case 'camel':
          return fileObj.name.toLowerCase().replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
            return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
          }).replace(/[\s\-_\.]+/g, '');
        case 'pascal':
          return fileObj.name.toLowerCase().replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter) {
            return letter.toUpperCase();
          }).replace(/[\s\-_\.]+/g, '');
        default:
          return fileObj.name;
      }
    }
  },
  'r': {
    name: 'RegEx',
    description: 'The match of the RegEx pattern specified in -r "..."',
    unique: false,
    function: function(fileObj) {
      if (fileObj.regexMatch) {
        return fileObj.regexMatch[0];
      } else {
        return '';
      }
    }
  },
  'p': {
    name: 'Parent directory',
    description: "The name of the parent directory",
    unique: false,
    function: function(fileObj) {
      return path.basename(fileObj.dir);
    }
  },
  'd': {
    name: 'Date',
    description: "The current date/time",
    parameters: {
      description: 'date format, default is yyyymmdd',
      default: 'yyyymmdd'
    },
    unique: false,
    function: function(fileObj, args) {
      let d = new Date();
      return dateFormat(d, args);
    }
  },
  'g': {
    name: 'GUID',
    description: "A globally unique identifier",
    unique: true,
    function: function() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
      });
    }
  },
  'eiso': {
    name: 'Exif ISO',
    description: "Photo ISO value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.SubExif && data.SubExif.PhotographicSensitivity ? data.SubExif.PhotographicSensitivity : '');
    }
  },
  'efnum': {
    name: 'Exif FNumber',
    description: "Photo FNumber value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      return (typeof(data) === 'object' && data.SubExif && data.SubExif.FNumber ? data.SubExif.FNumber[0] : '');
    }
  },
  'eex': {
    name: 'Exif Exposure Time',
    description: "Photo exposure time value",
    unique: false,
    function: function(fileObj) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      if (typeof(data) === 'object' && data.SubExif && data.SubExif.ExposureTime) {
        if (data.SubExif.ExposureTime[0] < 1) {
          return n2f(data.SubExif.ExposureTime[0]);
        } else {
          return data.SubExif.ExposureTime[0];
        }
      } else {
        return '';
      }
    }
  },
  'ed': {
    name: 'Exif Date',
    description: "The date/time photo was taken",
    parameters: {
      description: 'date format, default is yyyymmdd',
      default: 'yyyymmdd'
    },
    unique: false,
    function: function(fileObj, args) {
      let data = getExifData(fileObj.dir + '/' + fileObj.base);
      let formattedDate = data.DateTime.split(/:|\s/)[1] + '/' + data.DateTime.split(/:|\s/)[2] + '/' + data.DateTime.split(/:|\s/)[0] + ' ' + data.DateTime.split(/:|\s/)[3] + ':' + data.DateTime.split(/:|\s/)[4] + ':' + data.DateTime.split(/:|\s/)[5];
      return (typeof(data) === 'object' && data.DateTime ? dateFormat(formattedDate, args) : '');
    }
  }
};

function getExifData(file) {
  try {
    let data = exif.parseSync(file);
    return data;
  } catch (ex) {
    return '';
  }
}

function fileIndexString(total, index) { // append correct number of zeroes depending on total number of files
  let totString = '' + total;
  let returnString = '' + index;
  while (returnString.length < totString.length) {
    returnString = '0' + returnString;
  }
  return returnString;
}

module.exports = replacements;<|endoftext|>


<commit_before>const Promise = require('bluebird')
const httpRequest = require('request')
const toArray = require('stream-to-array')
const toArrayAsync = Promise.promisify(toArray)

const preview = (request, response, options) => {
  return new Promise((resolve, reject) => {
    const req = httpRequest.post(options.publicUriForPreview || 'http://jsreport.net/temp', (err, resp, body) => {
      if (err) {
        return reject(err)
      }
      const iframe = ''
      const title = request.template.name || 'jsreport'
      const html = '<title>' + title + '</title>' + iframe + ''
      response.content = Buffer.from(html)
      response.meta.contentType = 'text/html'
      // sometimes files is not completely flushed and excel online cannot find it immediately
      setTimeout(function () {
        resolve()
      }, 500)
    })

    const form = req.form()
    form.append('file', response.stream)
    response.meta.contentType = 'text/html'
  })
}

module.exports = async (reporter, request, response) => {
  let options = reporter.options.xlsx || {}
  if (request.options.preview && options.previewInExcelOnline !== false) {
    return preview(request, response, options)
  }

  response.meta.contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  response.meta.contentDisposition = 'inline; filename="report.xlsx"'
  response.meta.fileExtension = 'xlsx'

  response.content = Buffer.concat(await toArrayAsync(response.stream))
}
<commit_msg>remove providing content disposition<commit_after>const Promise = require('bluebird')
const httpRequest = require('request')
const toArray = require('stream-to-array')
const toArrayAsync = Promise.promisify(toArray)

const preview = (request, response, options) => {
  return new Promise((resolve, reject) => {
    const req = httpRequest.post(options.publicUriForPreview || 'http://jsreport.net/temp', (err, resp, body) => {
      if (err) {
        return reject(err)
      }
      const iframe = ''
      const title = request.template.name || 'jsreport'
      const html = '<title>' + title + '</title>' + iframe + ''
      response.content = Buffer.from(html)
      response.meta.contentType = 'text/html'
      // sometimes files is not completely flushed and excel online cannot find it immediately
      setTimeout(function () {
        resolve()
      }, 500)
    })

    const form = req.form()
    form.append('file', response.stream)
    response.meta.contentType = 'text/html'
  })
}

module.exports = async (reporter, request, response) => {
  let options = reporter.options.xlsx || {}
  if (request.options.preview && options.previewInExcelOnline !== false) {
    return preview(request, response, options)
  }

  response.meta.contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  response.meta.fileExtension = 'xlsx'

  response.content = Buffer.concat(await toArrayAsync(response.stream))
}
<|endoftext|>


<commit_before>/*jshint camelcase: false */
'use strict';

/**
 * Review server that listens to GitHub notifications.
 * (C) 2015 Alex Fernández.
 */

// requires
require('prototypes');
var fs = require('fs');
var Log = require('log');
var http = require('http');
var testing = require('testing');
var trello = require('./trello.js');

// globals
var log = new Log('info');

// constants
var FUNCTIONS = {
	created: 'createComment',
	opened: 'openPullRequest',
	closed: 'closePullRequest',
	reopened: 'reopenPullRequest',
};

/**
 * A review server, with the given options, see below on start().
 */
function ReviewServer(options)
{
	// self-reference
	var self = this;

	// attributes
	var board = new trello.Board(options);

	// init
	if (options.quiet)
	{
		log.level = 'notice';
	}
	if (options.debug)
	{
		log.level = 'debug';
	}

	/**
	 * Start the server.
	 * An optional callback will be called after the server has started.
	 */
	self.start = function(callback)
	{
		var server = http.createServer(listen);
		server.on('error', function(error)
		{
			if (error.code == 'EADDRINUSE')
			{
				return callback('Port ' + options.port + ' in use, please free it and retry again');
			}
			return callback('Could not start server on port ' + options.port + ': ' + error);
		});
		server.listen(options.port, function()
		{
			log.info('Listening on port %s', options.port);
			if (callback)
			{
				callback();
			}
		});
		return server;
	};

	/**
	 * Listen to an incoming request.
	 */
	function listen(request, response)
	{
		var body = '';
		request.on('data', function(data)
		{
			body += data.toString();
		});
		request.on('error', function(error)
		{
			log.error('Could not listen to request: %s', error);
		});
		request.on('end', function()
		{
			end(request.url, body, response);
		});
	}

	/**
	 * End the response now.
	 */
	function end(url, body, response)
	{
		if (!url.contains(options.secret))
		{
			var code = options.error || 500;
			response.writeHead(code);
			response.end('ERROR');
			return;
		}
		response.end('OK');
		var json;
		try
		{
			json = JSON.parse(body);
		}
		catch(exception)
		{
			log.info('Invalid message received: %s', body.substring(0, 20));
			return;
		}
		log.debug('Got message to %s: %s', url, JSON.stringify(json, null, '\t'));
		analyze(json);
	}

	function analyze(info)
	{
		if (!info.action || !info.repository)
		{
			return log.info('Missing action/repository in message: %s / %s', info.action, info.repository);
		}
		var operationName = FUNCTIONS[info.action];
		if (!operationName)
		{
			return handleUnknownAction(info);
		}
		var operation = self[operationName];
		operation(info);
	}

	self.createComment = function(info)
	{
		// create comment in card
		var repoName = info.repository.name;
		var sender = info.sender.login;
		var title = '[' + repoName + '] ' + info.issue.title;
		var body = info.comment.body;
		if (!body.contains('+1') && !body.contains('-1'))
		{
			return log.info('Comment created by %s on %s: %s', sender, title, body);
		}
		board.findCard(title, function(error, card)
		{
			if (error)
			{
				return log.error('Could not find card %s: %s', title, error);
			}
			if (!card)
			{
				return log.info('Could not find card for %s', title);
			}
			board.addComment(card.id, body, function(error)
			{
				if (error)
				{
					return log.error('Could not add comment to %s: %s', title, error);
				}
				log.info('Comment %s added to %s', body, title);
			});
		});
	};

	self.openPullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		var description = info.pull_request.body;
		var username = info.pull_request.user.login;
		board.createCard(title, description, options.inProgress, function(error)
		{
			if (error)
			{
				return log.error('Could not create card: %s', error);
			}
			return log.info('Created card for %s by %s', title, username);
		});
	};

	function getTitle(info)
	{
		if (!info.pull_request)
		{
			return log.info('Missing pull request to open');
		}
		var repoName = info.repository.name;
		return '[' + repoName + '] ' + info.pull_request.title;
	}

	self.closePullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		var destination = 'cancelled';
		if (info.pull_request.merged)
		{
			destination = 'merged';
		}
		var listId = options[destination];
		board.moveCard(title, listId, function(error)
		{
			if (error)
			{
				return log.error('Could not move closed card %s to %s: %s', title, destination, error);
			}
			return log.info('Moved card for %s to %s', title, destination);
		});
	};

	self.reopenPullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		board.moveCard(title, options.inProgress, function(error)
		{
			if (error)
			{
				return log.error('Could not move reopened card %s: %s', title, error);
			}
			return log.info('Moved card for %s to in progress', title);
		});
	};

	function handleUnknownAction(info)
	{
		var filename = __dirname + '/../samples/' + info.action + '-sample.json';
		fs.writeFile(filename, JSON.stringify(info, null, '\t'), function(error)
		{
			if (error)
			{
				return log.error('Could not write action %s to file %s: %s', info.action, filename, error);
			}
			log.info('Received unknown action %s, written to %s', info.action, filename);
		});
	}
}

/**
 * Start a review server. Options can contain:
 *	- token: consumer token for Trello.
 *	- key: app key for Trello.
 *	- secret: part of the URL to invoke.
 *	- port: the port to use.
 *	- quiet: do not log messages.
 *	- debug: log debug messages.
 * Other arguments:
 *	- callback: invoked after the server has started.
 */
exports.start = function(options, callback)
{
	var server = new ReviewServer(options);
	return server.start(callback);
};

function testStartServer(callback)
{
	var options = {
		port: 10530,
	};
	var server = exports.start(options, function(error)
	{
		testing.check(error, 'Could not start server', callback);
		server.close(function(error)
		{
			testing.check(error, 'Could not stop server', callback);
			testing.success(callback);
		});
	});
}

/**
 * Run the tests.
 */
exports.test = function(callback)
{
	testing.run([testStartServer], 5000, callback);
};

// start server if invoked directly
if (__filename == process.argv[1])
{
	exports.test(testing.show);
}

<commit_msg>Show a link to the complete repo in card.<commit_after>/*jshint camelcase: false */
'use strict';

/**
 * Review server that listens to GitHub notifications.
 * (C) 2015 Alex Fernández.
 */

// requires
require('prototypes');
var fs = require('fs');
var Log = require('log');
var http = require('http');
var testing = require('testing');
var trello = require('./trello.js');

// globals
var log = new Log('info');

// constants
var FUNCTIONS = {
	created: 'createComment',
	opened: 'openPullRequest',
	closed: 'closePullRequest',
	reopened: 'reopenPullRequest',
};

/**
 * A review server, with the given options, see below on start().
 */
function ReviewServer(options)
{
	// self-reference
	var self = this;

	// attributes
	var board = new trello.Board(options);

	// init
	if (options.quiet)
	{
		log.level = 'notice';
	}
	if (options.debug)
	{
		log.level = 'debug';
	}

	/**
	 * Start the server.
	 * An optional callback will be called after the server has started.
	 */
	self.start = function(callback)
	{
		var server = http.createServer(listen);
		server.on('error', function(error)
		{
			if (error.code == 'EADDRINUSE')
			{
				return callback('Port ' + options.port + ' in use, please free it and retry again');
			}
			return callback('Could not start server on port ' + options.port + ': ' + error);
		});
		server.listen(options.port, function()
		{
			log.info('Listening on port %s', options.port);
			if (callback)
			{
				callback();
			}
		});
		return server;
	};

	/**
	 * Listen to an incoming request.
	 */
	function listen(request, response)
	{
		var body = '';
		request.on('data', function(data)
		{
			body += data.toString();
		});
		request.on('error', function(error)
		{
			log.error('Could not listen to request: %s', error);
		});
		request.on('end', function()
		{
			end(request.url, body, response);
		});
	}

	/**
	 * End the response now.
	 */
	function end(url, body, response)
	{
		if (!url.contains(options.secret))
		{
			var code = options.error || 500;
			response.writeHead(code);
			response.end('ERROR');
			return;
		}
		response.end('OK');
		var json;
		try
		{
			json = JSON.parse(body);
		}
		catch(exception)
		{
			log.info('Invalid message received: %s', body.substring(0, 20));
			return;
		}
		log.debug('Got message to %s: %s', url, JSON.stringify(json, null, '\t'));
		analyze(json);
	}

	function analyze(info)
	{
		if (!info.action || !info.repository)
		{
			return log.info('Missing action/repository in message: %s / %s', info.action, info.repository);
		}
		var operationName = FUNCTIONS[info.action];
		if (!operationName)
		{
			return handleUnknownAction(info);
		}
		var operation = self[operationName];
		operation(info);
	}

	self.createComment = function(info)
	{
		// create comment in card
		var repoName = info.repository.name;
		var sender = info.sender.login;
		var title = '[' + repoName + '] ' + info.issue.title;
		var body = info.comment.body;
		if (!body.contains('+1') && !body.contains('-1'))
		{
			return log.info('Comment created by %s on %s: %s', sender, title, body);
		}
		board.findCard(title, function(error, card)
		{
			if (error)
			{
				return log.error('Could not find card %s: %s', title, error);
			}
			if (!card)
			{
				return log.info('Could not find card for %s', title);
			}
			board.addComment(card.id, body, function(error)
			{
				if (error)
				{
					return log.error('Could not add comment to %s: %s', title, error);
				}
				log.info('Comment %s added to %s', body, title);
			});
		});
	};

	self.openPullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		var link = '[View on GitHub](' + info.pull_request.html_url + ')\n';
		var description = link + info.pull_request.body;
		var username = info.pull_request.user.login;
		board.createCard(title, description, options.inProgress, function(error)
		{
			if (error)
			{
				return log.error('Could not create card: %s', error);
			}
			return log.info('Created card for %s by %s', title, username);
		});
	};

	function getTitle(info)
	{
		if (!info.pull_request)
		{
			return log.info('Missing pull request to open');
		}
		var repoName = info.repository.name;
		return '[' + repoName + '] ' + info.pull_request.title;
	}

	self.closePullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		var destination = 'cancelled';
		if (info.pull_request.merged)
		{
			destination = 'merged';
		}
		var listId = options[destination];
		board.moveCard(title, listId, function(error)
		{
			if (error)
			{
				return log.error('Could not move closed card %s to %s: %s', title, destination, error);
			}
			return log.info('Moved card for %s to %s', title, destination);
		});
	};

	self.reopenPullRequest = function(info)
	{
		var title = getTitle(info);
		if (!title)
		{
			return;
		}
		board.moveCard(title, options.inProgress, function(error)
		{
			if (error)
			{
				return log.error('Could not move reopened card %s: %s', title, error);
			}
			return log.info('Moved card for %s to in progress', title);
		});
	};

	function handleUnknownAction(info)
	{
		var filename = __dirname + '/../samples/' + info.action + '-sample.json';
		fs.writeFile(filename, JSON.stringify(info, null, '\t'), function(error)
		{
			if (error)
			{
				return log.error('Could not write action %s to file %s: %s', info.action, filename, error);
			}
			log.info('Received unknown action %s, written to %s', info.action, filename);
		});
	}
}

/**
 * Start a review server. Options can contain:
 *	- token: consumer token for Trello.
 *	- key: app key for Trello.
 *	- secret: part of the URL to invoke.
 *	- port: the port to use.
 *	- quiet: do not log messages.
 *	- debug: log debug messages.
 * Other arguments:
 *	- callback: invoked after the server has started.
 */
exports.start = function(options, callback)
{
	var server = new ReviewServer(options);
	return server.start(callback);
};

function testStartServer(callback)
{
	var options = {
		port: 10530,
	};
	var server = exports.start(options, function(error)
	{
		testing.check(error, 'Could not start server', callback);
		server.close(function(error)
		{
			testing.check(error, 'Could not stop server', callback);
			testing.success(callback);
		});
	});
}

/**
 * Run the tests.
 */
exports.test = function(callback)
{
	testing.run([testStartServer], 5000, callback);
};

// start server if invoked directly
if (__filename == process.argv[1])
{
	exports.test(testing.show);
}

<|endoftext|>


<commit_before>var SSE = require('./sse');

var RouteMethod = function(options) {
  this.options = options;

  RouteMethod.prototype.register = function() {
    var handlers = this.options.handlers;
    var route = this.options.route;
    var method = this.options.method;

    if (this.options.sse) {
      SSE.initRoute(route);
      return this;
    }

    if (this.options.notAllowed) {
        this.options.router[method](route, methodNotAllowed);
    } else {
      this.options.router[method](this.options.route, handlers[route][method]);
    }
  };

  RouteMethod.prototype.authorize = function(authorizeFunc) {
    this.authorizeFunc = authorizeFunc;
    return this;
  };

  RouteMethod.prototype.before = function(beforeFunc) {
    this.beforeFunc = beforeFunc;
    return this;
  };

  RouteMethod.prototype.after = function(afterFunc) {
    this.afterFunc = beforeFunc;
    return this;
  };

  var methodNotAllowed = function (req, res) {
      sendError(req, res, new JSONAPI_Error({status: 405}));
  };
};

module.exports = RouteMethod;<commit_msg>added the handler function<commit_after>var SSE = require('./sse');

var RouteMethod = function(options) {
  this.options = options;

  RouteMethod.prototype.register = function() {
    var handlers = this.options.handlers;
    var route = this.options.route;
    var method = this.options.method;

    this.handler = methodNotAllowed;

    if (this.options.sse) {
      SSE.initRoute(route);
      return this;
    }

    if (this.options.notAllowed) {
        this.options.router[method](route, this.handler);
    } else {
      this.handler = handlers[route][method];
      this.options.router[method](this.options.route, this.handler);
    }
  };

  RouteMethod.prototype.authorize = function(authorizeFunc) {
    this.authorizeFunc = authorizeFunc;
    return this;
  };

  RouteMethod.prototype.before = function(beforeFunc) {
    this.beforeFunc = beforeFunc;
    return this;
  };

  RouteMethod.prototype.after = function(afterFunc) {
    this.afterFunc = beforeFunc;
    return this;
  };

  RouteMethod.prototype.handler = function() {
    return this.handler;
  };

  var methodNotAllowed = function (req, res) {
      sendError(req, res, new JSONAPI_Error({status: 405}));
  };
};

module.exports = RouteMethod;<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var Readable = require('stream').Readable;
var QS = require('querystring');
var _ = require('lodash');
var Express = require('express');

var buildReq = require('./req');
var buildRes = require('./res');
var defaultHandlers = require('./bindDefaultHandlers');

/**
 * Expose new instance of `Router`
 *
 * @api private
 */
module.exports = function(sails) {
  return new Router({sails: sails});
};

/**
 * Initialize a new `Router`
 *
 * @param {Object} options
 * @api private
 */

function Router(options) {

  options = options || {};
  this.sails = options.sails;
  this.defaultHandlers = defaultHandlers(this.sails);

  // Expose router on `sails` object
  this.sails.router = this;

  // Required for dynamic NODE_ENV setting via command line args
  // TODO:
  // instead, use: https://www.npmjs.org/package/path-to-regexp
  // (or: https://www.npmjs.org/package/path-match)
  this._privateRouter = Express();

  // Bind the context of all instance methods
  this.load = _.bind(this.load, this);
  this.bind = _.bind(this.bind, this);
  this.unbind = _.bind(this.unbind, this);
  this.reset = _.bind(this.reset, this);
  this.flush = _.bind(this.flush, this);
  this.route = _.bind(this.route, this);
}

/**
 * _privateRouter
 *
 * This internal "private" instance of an Express app object
 * is used only for routing. (i.e. it will not be used for
 * listening to actual HTTP requests; instead, one or more
 * delegate servers can be attached- see the `http` or
 * `sockets` hooks for examples of attaching a server to
 * Sails)
 *
 * NOTE: Requires calling `load()` before use in order to
 * provide access to the proper NODE_ENV, since Express
 * uses that to determine its environment (development vs.
 * production.)
 */

// Router.prototype._privateRouter;

/**
 * `sails.router.load()`
 *
 * Expose the router, create the Express private router,
 * then call flush(), which will bind configured routes
 * and emit the appropriate events.
 *
 * @api public
 */

Router.prototype.load = function(cb) {
  var sails = this.sails;

  sails.log.verbose('Loading router...');

  // Maintain a reference to the static route config
  this.explicitRoutes = sails.config.routes;

  // Save reference to sails logger
  this.log = sails.log;

  // If a session store is configured, hook it up as `req.session` by passing
  // it down to the session middleware
  if (sails.hooks.session) {
    sails.after('hook:session:loaded', function (){
      // if (!sails.config.session || !sails.config.session.store || !sails.config.session.secret) {
      //   return cb(new Error('Consistency violation: expected session store+secret config to exist if the session hook is enabled. Is `sails.config.session` valid?'));
      // }
      sails._privateCpMware = Express.cookieParser(sails.config.session.secret);
      sails._privateSessionMiddleware = Express.session(sails.config.session);
    });
  }

  // Wipe any existing routes and bind them anew
  this.flush();

  // Listen for requests
  sails.on('router:request', this.route);

  // Listen for unhandled errors and unmatched routes
  sails.on('router:request:500', this.defaultHandlers[500]);
  sails.on('router:request:404', this.defaultHandlers[404]);

  cb();
};

/**
 * `sails.router.route(partialReq, partialRes)`
 *
 * Interpret the specified (usually partial) request and response objects into
 * streams with all of the expected methods, then routes the fully-formed request
 * using the built-in private router. Useful for creating virtual request/response
 * streams from non-HTTP sources, like Socket.io or unit tests.
 *
 * This method is not always helpful-- it is not called for HTTP requests, for instance,
 * since the true HTTP req/res streams already exist.  In that case, at lift-time, Sails
 * calls `router:bind`, which loads Sails' routes as normal middleware/routes in the http hook.
 * stack will run as usual.
 *
 * On the other hand, Socket.io needs to use this method (i.e. the `router:request` event)
 * to simulate a connect-style router since it can't bind dynamic routes ahead of time.
 *
 * Keep in mind that, if `route` is not used, the implementing server is responsible
 * for routing to Sails' default `next(foo)` handler.
 *
 * @param {Request} req
 * @param {Response} res
 * @api private
 */

Router.prototype.route = function(req, res) {
  var sails = this.sails;
  var _privateRouter = this._privateRouter;

  // If sails is `_exiting`, ignore the request.
  if (sails._exiting) {
    return;
  }

  // Provide access to `sails` object
  req._sails = req._sails || sails;

  // Use base req and res definitions to ensure the specified
  // objects are at least ducktype-able as standard node HTTP
  // req and req streams.
  //
  // Make sure request and response objects have reasonable defaults
  // (will use the supplied definitions if possible)
  req = buildReq(req, res);
  res = buildRes(req, res);

  // console.log('\n\n\n\n=======================\nReceived request to %s %s\nwith req.body:\n',req.method,req.url, req.body);

  // Deprecation error:
  res._cb = function _cbIsDeprecated(err) {
    throw new Error('As of v0.10, `_cb()` shim is no longer supported in the Sails router.');
  };

  // Run some basic middleware
  sails.log.silly('Handling virtual request :: Running virtual querystring parser...');
  qsParser(req,res, function (err) {
    if (err) {
      return res.send(400, err && err.stack);
    }

    // Parse cookies
    parseCookies(req, res, function(err){
      if (err) {
        return res.send(400, err && err.stack);
      }

      // console.log('Ran cookie parser');
      // console.log('res.writeHead= ',res.writeHead);

      // Load session (if relevant)
      loadSession(req, res, function (err) {
        if (err) {
          return res.send(400, err && err.stack);
        }
        // console.log('res is now:\n',res);
        // console.log('\n\n');
        // console.log('Ran session middleware');
        // console.log('req.sessionID= ',req.sessionID);
        // console.log('The loaded req.session= ',req.session);

        sails.log.silly('Handling virtual request :: Running virtual body parser...');
        bodyParser(req,res, function (err) {
          if (err) {
            return res.send(400, err && err.stack);
          }

          // Use our private router to route the request
          _privateRouter.router(req, res, function handleUnmatchedNext(err) {
            //
            // In the event of an unmatched `next()`, `next('foo')`,
            // or `next('foo', errorCode)`...
            //

            // Use the default server error handler
            if (err) {
              sails.log.silly('Handling virtual request :: Running final "error" handler...');
              sails.emit('router:request:500', err, req, res);
              return;
            }

            // Or the default not found handler
            sails.log.silly('Handling virtual request :: Running final "not found" handler...');
            sails.emit('router:request:404', req, res);
            return;
          });
        });
      });

    });
  });

};

/**
 * `sails.router.bind()`
 *
 * Bind new route(s)
 *
 * @param {String|RegExp} path
 * @param {String|Object|Array|Function} bindTo
 * @param {String} verb
 * @api private
 */

Router.prototype.bind = require('./bind');

/**
 * `sails.router.unbind()`
 *
 * Unbind existing route
 *
 * @param {Object} route
 * @api private
 */

Router.prototype.unbind = function(route) {

  var sails = this.sails;

  // Inform attached servers that route should be unbound
  sails.emit('router:unbind', route);

  // Remove route in internal router
  var newRoutes = [];
  _.each(this._privateRouter.routes[route.method], function(expressRoute) {
    if (expressRoute.path != route.path) {
      newRoutes.push(expressRoute);
    }
  });
  this._privateRouter.routes[route.method] = newRoutes;

};

/**
 * `sails.router.reset()`
 *
 * Unbind all routes currently attached to the router
 *
 * @api private
 */

Router.prototype.reset = function() {
  var sails = this.sails;

  // Unbind everything
  _.each(this._privateRouter.routes, function(routes, httpMethod) {

    // Unbind each route for the specified HTTP verb
    var routesToUnbind = this._privateRouter.routes[httpMethod] || [];
    _.each(routesToUnbind, this.unbind, this);

  }, this);

  // Emit reset event to allow attached servers to
  // unbind all of their routes as well
  sails.emit('router:reset');

};

/**
 * `sails.router.flush()`
 *
 * Unbind all current routes, then re-bind everything, re-emitting the routing
 * lifecycle events (e.g. `router:before` and `router:after`)
 *
 * @param {Object} routes - (optional)
 *  If specified, replaces `this.explicitRoutes` before flushing.
 *
 * @api private
 */

Router.prototype.flush = function(routes) {
  var sails = this.sails;

  // Wipe routes
  this.reset();

  // Fired before static routes are bound
  sails.emit('router:before');

  // If specified, replace `this.explicitRoutes`
  if (routes) {
    this.explicitRoutes = routes;
  }

  // Use specified path to bind static routes
  _.each(this.explicitRoutes, function(target, path) {
    this.bind(path, target);
  }, this);

  // Fired after static routes are bound
  sails.emit('router:after');
};

////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ||     Private functions
// \/
//
////////////////////////////////////////////////////////////////////////////////////////////////////

// Extremely simple query string parser (`req.query`)
function qsParser(req,res,next) {
  var queryStringPos = req.url.indexOf('?');
  if (queryStringPos !== -1) {
    req.query = _.merge(req.query, QS.parse(req.url.substr(queryStringPos + 1)));
  }
  else {
    req.query = req.query || {};
  }
  next();
}
// Extremely simple body parser (`req.body`)
function bodyParser (req, res, next) {
  var bodyBuffer='';
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'){
    req.body = _.extend({}, req.body);
    return next();
  }

  // Ensure that `req` is a readable stream at this point
  if ( ! req instanceof Readable ) {
    return next(new Error('Sails Internal Error: `req` should be a Readable stream by the time `route()` is called'));
  }

  req.on('readable', function() {
    var chunk;
    while (null !== (chunk = req.read())) {
      bodyBuffer += chunk;
    }
  });
  req.on('end', function() {

    var parsedBody;
    try {
      parsedBody = JSON.parse(bodyBuffer);
    } catch (e) {}

    req.body = _.merge(req.body, parsedBody);
    next();
  });
}

/**
 * [parseCookies description]
 * @param  {[type]}   req  [description]
 * @param  {[type]}   res  [description]
 * @param  {Function} next [description]
 * @return {[type]}        [description]
 */
function parseCookies (req, res, next){

  // req._sails.log.verbose('Parsing cookie:',req.headers.cookie);

  if (req._sails._privateCpMware) {
    // Run the middleware
    return req._sails._privateCpMware(req, res, next);
  }

  // Otherwise don't even worry about it.
  return next();
}

/**
 * [loadSession description]
 * @param  {[type]}   req  [description]
 * @param  {[type]}   res  [description]
 * @param  {Function} next [description]
 * @return {[type]}        [description]
 */
function loadSession (req, res, next){

  // If a session store is configured, hook it up as `req.session` by passing
  // it down to the session middleware
  if (req._sails._privateSessionMiddleware) {

    // Access store preconfigured session middleware as a private property on the app instance.
    return req._sails._privateSessionMiddleware(req, res, next);
  }

  // Otherwise don't even worry about it.
  return next();
}
<commit_msg>Allow disabling of session on a per-request basis in the virtual router<commit_after>/**
 * Module dependencies.
 */

var Readable = require('stream').Readable;
var QS = require('querystring');
var _ = require('lodash');
var Express = require('express');

var buildReq = require('./req');
var buildRes = require('./res');
var defaultHandlers = require('./bindDefaultHandlers');

/**
 * Expose new instance of `Router`
 *
 * @api private
 */
module.exports = function(sails) {
  return new Router({sails: sails});
};

/**
 * Initialize a new `Router`
 *
 * @param {Object} options
 * @api private
 */

function Router(options) {

  options = options || {};
  this.sails = options.sails;
  this.defaultHandlers = defaultHandlers(this.sails);

  // Expose router on `sails` object
  this.sails.router = this;

  // Required for dynamic NODE_ENV setting via command line args
  // TODO:
  // instead, use: https://www.npmjs.org/package/path-to-regexp
  // (or: https://www.npmjs.org/package/path-match)
  this._privateRouter = Express();

  // Bind the context of all instance methods
  this.load = _.bind(this.load, this);
  this.bind = _.bind(this.bind, this);
  this.unbind = _.bind(this.unbind, this);
  this.reset = _.bind(this.reset, this);
  this.flush = _.bind(this.flush, this);
  this.route = _.bind(this.route, this);
}

/**
 * _privateRouter
 *
 * This internal "private" instance of an Express app object
 * is used only for routing. (i.e. it will not be used for
 * listening to actual HTTP requests; instead, one or more
 * delegate servers can be attached- see the `http` or
 * `sockets` hooks for examples of attaching a server to
 * Sails)
 *
 * NOTE: Requires calling `load()` before use in order to
 * provide access to the proper NODE_ENV, since Express
 * uses that to determine its environment (development vs.
 * production.)
 */

// Router.prototype._privateRouter;

/**
 * `sails.router.load()`
 *
 * Expose the router, create the Express private router,
 * then call flush(), which will bind configured routes
 * and emit the appropriate events.
 *
 * @api public
 */

Router.prototype.load = function(cb) {
  var sails = this.sails;

  sails.log.verbose('Loading router...');

  // Maintain a reference to the static route config
  this.explicitRoutes = sails.config.routes;

  // Save reference to sails logger
  this.log = sails.log;

  // If a session store is configured, hook it up as `req.session` by passing
  // it down to the session middleware
  if (sails.hooks.session) {
    sails.after('hook:session:loaded', function (){
      // if (!sails.config.session || !sails.config.session.store || !sails.config.session.secret) {
      //   return cb(new Error('Consistency violation: expected session store+secret config to exist if the session hook is enabled. Is `sails.config.session` valid?'));
      // }
      sails._privateCpMware = Express.cookieParser(sails.config.session.secret);
      sails._privateSessionMiddleware = Express.session(sails.config.session);
    });
  }

  // Wipe any existing routes and bind them anew
  this.flush();

  // Listen for requests
  sails.on('router:request', this.route);

  // Listen for unhandled errors and unmatched routes
  sails.on('router:request:500', this.defaultHandlers[500]);
  sails.on('router:request:404', this.defaultHandlers[404]);

  cb();
};

/**
 * `sails.router.route(partialReq, partialRes)`
 *
 * Interpret the specified (usually partial) request and response objects into
 * streams with all of the expected methods, then routes the fully-formed request
 * using the built-in private router. Useful for creating virtual request/response
 * streams from non-HTTP sources, like Socket.io or unit tests.
 *
 * This method is not always helpful-- it is not called for HTTP requests, for instance,
 * since the true HTTP req/res streams already exist.  In that case, at lift-time, Sails
 * calls `router:bind`, which loads Sails' routes as normal middleware/routes in the http hook.
 * stack will run as usual.
 *
 * On the other hand, Socket.io needs to use this method (i.e. the `router:request` event)
 * to simulate a connect-style router since it can't bind dynamic routes ahead of time.
 *
 * Keep in mind that, if `route` is not used, the implementing server is responsible
 * for routing to Sails' default `next(foo)` handler.
 *
 * @param {Request} req
 * @param {Response} res
 * @api private
 */

Router.prototype.route = function(req, res) {
  var sails = this.sails;
  var _privateRouter = this._privateRouter;

  // If sails is `_exiting`, ignore the request.
  if (sails._exiting) {
    return;
  }

  // Provide access to `sails` object
  req._sails = req._sails || sails;

  // Use base req and res definitions to ensure the specified
  // objects are at least ducktype-able as standard node HTTP
  // req and req streams.
  //
  // Make sure request and response objects have reasonable defaults
  // (will use the supplied definitions if possible)
  req = buildReq(req, res);
  res = buildRes(req, res);

  // console.log('\n\n\n\n=======================\nReceived request to %s %s\nwith req.body:\n',req.method,req.url, req.body);

  // Deprecation error:
  res._cb = function _cbIsDeprecated(err) {
    throw new Error('As of v0.10, `_cb()` shim is no longer supported in the Sails router.');
  };

  // Run some basic middleware
  sails.log.silly('Handling virtual request :: Running virtual querystring parser...');
  qsParser(req,res, function (err) {
    if (err) {
      return res.send(400, err && err.stack);
    }

    // Parse cookies
    parseCookies(req, res, function(err){
      if (err) {
        return res.send(400, err && err.stack);
      }

      // console.log('Ran cookie parser');
      // console.log('res.writeHead= ',res.writeHead);

      // Load session (if relevant)
      loadSession(req, res, function (err) {
        if (err) {
          return res.send(400, err && err.stack);
        }
        // console.log('res is now:\n',res);
        // console.log('\n\n');
        // console.log('Ran session middleware');
        // console.log('req.sessionID= ',req.sessionID);
        // console.log('The loaded req.session= ',req.session);

        sails.log.silly('Handling virtual request :: Running virtual body parser...');
        bodyParser(req,res, function (err) {
          if (err) {
            return res.send(400, err && err.stack);
          }

          // Use our private router to route the request
          _privateRouter.router(req, res, function handleUnmatchedNext(err) {
            //
            // In the event of an unmatched `next()`, `next('foo')`,
            // or `next('foo', errorCode)`...
            //

            // Use the default server error handler
            if (err) {
              sails.log.silly('Handling virtual request :: Running final "error" handler...');
              sails.emit('router:request:500', err, req, res);
              return;
            }

            // Or the default not found handler
            sails.log.silly('Handling virtual request :: Running final "not found" handler...');
            sails.emit('router:request:404', req, res);
            return;
          });
        });
      });

    });
  });

};

/**
 * `sails.router.bind()`
 *
 * Bind new route(s)
 *
 * @param {String|RegExp} path
 * @param {String|Object|Array|Function} bindTo
 * @param {String} verb
 * @api private
 */

Router.prototype.bind = require('./bind');

/**
 * `sails.router.unbind()`
 *
 * Unbind existing route
 *
 * @param {Object} route
 * @api private
 */

Router.prototype.unbind = function(route) {

  var sails = this.sails;

  // Inform attached servers that route should be unbound
  sails.emit('router:unbind', route);

  // Remove route in internal router
  var newRoutes = [];
  _.each(this._privateRouter.routes[route.method], function(expressRoute) {
    if (expressRoute.path != route.path) {
      newRoutes.push(expressRoute);
    }
  });
  this._privateRouter.routes[route.method] = newRoutes;

};

/**
 * `sails.router.reset()`
 *
 * Unbind all routes currently attached to the router
 *
 * @api private
 */

Router.prototype.reset = function() {
  var sails = this.sails;

  // Unbind everything
  _.each(this._privateRouter.routes, function(routes, httpMethod) {

    // Unbind each route for the specified HTTP verb
    var routesToUnbind = this._privateRouter.routes[httpMethod] || [];
    _.each(routesToUnbind, this.unbind, this);

  }, this);

  // Emit reset event to allow attached servers to
  // unbind all of their routes as well
  sails.emit('router:reset');

};

/**
 * `sails.router.flush()`
 *
 * Unbind all current routes, then re-bind everything, re-emitting the routing
 * lifecycle events (e.g. `router:before` and `router:after`)
 *
 * @param {Object} routes - (optional)
 *  If specified, replaces `this.explicitRoutes` before flushing.
 *
 * @api private
 */

Router.prototype.flush = function(routes) {
  var sails = this.sails;

  // Wipe routes
  this.reset();

  // Fired before static routes are bound
  sails.emit('router:before');

  // If specified, replace `this.explicitRoutes`
  if (routes) {
    this.explicitRoutes = routes;
  }

  // Use specified path to bind static routes
  _.each(this.explicitRoutes, function(target, path) {
    this.bind(path, target);
  }, this);

  // Fired after static routes are bound
  sails.emit('router:after');
};

////////////////////////////////////////////////////////////////////////////////////////////////////
//
// ||     Private functions
// \/
//
////////////////////////////////////////////////////////////////////////////////////////////////////

// Extremely simple query string parser (`req.query`)
function qsParser(req,res,next) {
  var queryStringPos = req.url.indexOf('?');
  if (queryStringPos !== -1) {
    req.query = _.merge(req.query, QS.parse(req.url.substr(queryStringPos + 1)));
  }
  else {
    req.query = req.query || {};
  }
  next();
}
// Extremely simple body parser (`req.body`)
function bodyParser (req, res, next) {
  var bodyBuffer='';
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'DELETE'){
    req.body = _.extend({}, req.body);
    return next();
  }

  // Ensure that `req` is a readable stream at this point
  if ( ! req instanceof Readable ) {
    return next(new Error('Sails Internal Error: `req` should be a Readable stream by the time `route()` is called'));
  }

  req.on('readable', function() {
    var chunk;
    while (null !== (chunk = req.read())) {
      bodyBuffer += chunk;
    }
  });
  req.on('end', function() {

    var parsedBody;
    try {
      parsedBody = JSON.parse(bodyBuffer);
    } catch (e) {}

    req.body = _.merge(req.body, parsedBody);
    next();
  });
}

/**
 * [parseCookies description]
 * @param  {[type]}   req  [description]
 * @param  {[type]}   res  [description]
 * @param  {Function} next [description]
 * @return {[type]}        [description]
 */
function parseCookies (req, res, next){

  // req._sails.log.verbose('Parsing cookie:',req.headers.cookie);

  if (req._sails._privateCpMware) {
    // Run the middleware
    return req._sails._privateCpMware(req, res, next);
  }

  // Otherwise don't even worry about it.
  return next();
}

/**
 * [loadSession description]
 * @param  {[type]}   req  [description]
 * @param  {[type]}   res  [description]
 * @param  {Function} next [description]
 * @return {[type]}        [description]
 */
function loadSession (req, res, next){

  // If a session store is configured, and we haven't deliberately disabled
  // session support for this request by setting the "nosession" header,
  // hook up the store up as `req.session` by passing it down to the
  // session middleware.
  if (req._sails._privateSessionMiddleware && !req.headers.nosession) {

    // Access store preconfigured session middleware as a private property on the app instance.
    return req._sails._privateSessionMiddleware(req, res, next);
  }

  // Otherwise don't even worry about it.
  return next();
}
<|endoftext|>