<commit_before>const fs = require('fs')
const os = require('os')
const path = require('path')
const net = require('net')

const { promisify } = require('util')
const { Transform, Writable } = require('stream')

const socketStream = require('socket.io-stream')

const readFile = promisify(fs.readFile)
const Cache = require('./cache')
const { retry, uuidv4 } = require('./utils')

class DownloadStream extends Transform {
  _transform(chunk, encoding, next) {
    this.push(chunk)
    next()
  }
}

class Handler {
  wrap(key) {
    const method = this[key].bind(this)
    return async(data, ack) => {
      try {
        ack({
          status: 'ok',
          data: await method(data),
        })
      } catch (err) {
        console.error('Uncaught RPC', err.stack || err)
        console.error('method:', key, 'args:', data)
        ack({
          status: 'error',
          error: `${err}`,
        })
      }
    }
  }

  handleError(message, error) {
    console.error('error message from frida')
    console.error(message.stack || message)
    console.error(error)
  }

  async getAgent(name) {
    const prefix = path.join(__dirname, '..', 'agent', `${name}.bundle`)
    if (process.env.NODE_ENV === 'development') {
      const source = await readFile(`${prefix}.js`, 'utf8')
      return this.session.createScript(source)
    }
    const bytes = await readFile(`${prefix}.bin`)
    return this.session.createScriptFromBytes(bytes)
  }
}

class BannerStream extends Writable {
  constructor(opt) {
    super(opt)
    this.socket = opt.socket
  }

  _write(chunk, encoding, callback) {
    this.socket.write(chunk, callback)
  }
}

class RpcHandler extends Handler {
  constructor(session, socket) {
    super()

    this.session = session
    this.socket = socket
    this.stream = socketStream(socket)
    this.cache = new Cache()
    this.transfer = new Map()
    this.userScripts = new Map()
    this.handleMessage()

    this.syslogServer = null
    this.syslogClients = new Set()
    this.syslogFile = path.join(os.tmpdir(), Math.random().toString(16).slice(2))
  }

  async createSysLogServer() {
    const server = net.createServer((client) => {
      const ip = client.address().address.green
      this.syslogClients.add(client)
      console.log(`new client ${ip}`)
      const writable = new BannerStream({ socket: client })
      fs.createReadStream(this.syslogFile).pipe(writable)
      client.on('end', () => {
        console.log(`client ${ip} disconnected`)
        this.syslogClients.delete(client)
      })
    })
    this.syslogServer = server
    return new Promise((resolve, reject) => {
      server.listen({ host: 'localhost', port: 0 }, () => {
        console.log(`nc localhost ${server.address().port}`.yellow)
        resolve()
      }).on('error', reject)
    })
  }

  async load() {
    const { socket } = this
    const script = await this.getAgent('app')
    script.destroyed.connect(() => {
      socket.emit('script_destroyed')
      socket.disconnect(true)
    })
    script.message.connect((message, data) => {
      const mapping = {
        send: this.handleSend,
        error: this.handleError,
      }
      const handler = mapping[message.type]
      if (typeof handler === 'function')
        handler.call(this, message, data)
      else
        console.warn(`unhandled message type: ${message.type}`.yellow)
    })

    await script.load()
    this.script = script
    this.agent = script.exports

    await this.createSysLogServer()
  }

  handleSend({ payload }, data) {
    if (payload.subject === 'download') {
      const { event, session } = payload
      if (event === 'start') {
        const stream = new DownloadStream()
        this.transfer.set(session, stream)
      } else if (event === 'end') {
        const stream = this.transfer.get(session)
        stream.end()
      } else if (event === 'data') {
        const stream = this.transfer.get(session)
        stream.write(data)
      }
    } else if (payload.subject === 'syslog') {
      fs.appendFile(this.syslogFile, data, () => {})
      this.syslogClients.forEach(client => client.write(data))
    } else {
      if (data !== null) {
        console.warn('does not support binary protocol yet, message payload:'.yellow)
        console.warn(payload)
      }

      this.socket.emit('console', payload) // forward to browser
    }
  }

  handleMessage() {
    [
      'eval',
      'unload',

      'modules',
      'exports',

      'classes',
      'ownClasses',
      'inspect',

      'info',
      'userDefaults',
      'imports',

      'ls',
      'plist',
      'text',
      'download',

      'tables',
      'data',

      'dumpWindow',
      'toggleDebugOverlay',

      'dumpKeyChain',
      'cookies',

      'hook',
      'unhook',
      'swizzle',
      'unswizzle',

      'dumpdecrypted',
      'screenshot',
    ].forEach(event => this.socket.on(event, this.wrap(event)))

    // handle file transfer
    this.stream.on('download', (stream, args) => {
      const { session } = args
      if (session && this.transfer.has(session)) {
        const source = this.transfer.get(session)
        source.pipe(stream).on('finish', () => this.transfer.delete(session))
      }
    })
  }

  async eval(source) {
    const uuid = uuidv4()
    const { socket } = this
    const script = await this.session.createScript(`
      rpc.exports.bootstrap = function(js) {
        // temp workaround for
        // https://github.com/frida/frida-node/pull/28
        // in case the output goes to server side console instead
        // being sent to frontend
        //
        // this is not a sandbox, do not waste your time on escaping it

        ['log', 'warn', 'error'].forEach(function(level) {
          console[level] = function() {
            send({
              subject: 'console.message',
              level: level,
              args: [].slice.call(arguments)
            });
          };
        });

        // wow, copied from frida-python
        try {
          const result = (1, eval)(js);
          if (result instanceof ArrayBuffer) {
            return result;
          } else {
            var type = (result === null) ? 'null' : typeof result;
            return [type, result];
          }
        } catch (e) {
          return ['error', e instanceof Error ? {
            name: e.name,
            message: e.message,
            stack: e.stack
          } : e + ''];
        }
      }
    `)

    script.destroyed.connect(() => {
      socket.emit('userScript', {
        subject: 'destroyed',
        uuid,
      })
    })

    script.message.connect((message, data) => {
      const { type, payload } = message
      // forward to frontend
      socket.emit('userScript', {
        subject: 'message',
        uuid,
        type,
        payload,
        // binary data is not supported right now
        hasData: data !== null,
      })
    })

    try {
      await script.load()
      const result = await script.exports.bootstrap(source)
      let [type, value] = result
      if (result instanceof Buffer) {
        type = 'arraybuffer'
        value = Buffer.from(result).toString('base64')
      }

      this.userScripts.set(uuid, script)

      if (type === 'error') {
        console.error('Uncaught user frida script', value.stack || value)
        return {
          status: 'failed',
          error: value,
        }
      }

      return {
        status: 'ok',
        uuid,
        type,
        value,
      }
    } catch (error) {
      console.error('Uncaught user frida script', error.stack || error)
      return {
        status: 'failed',
        error,
      }
    }
  }

  async unload(uuid) {
    const script = this.userScripts.get(uuid)
    if (script) {
      this.userScripts.delete(uuid)
      return script.unload()
    }
    this.syslogClients.forEach(client => client.close())
    throw new Error(`script not found: ${uuid}`)
  }

  async modules() {
    return this.cache.fetch('modules', () => this.agent.modules())
  }

  async exports({ module }) {
    return this.agent.exports(module)
  }

  async classes() {
    return this.cache.fetch('classes', () => this.agent.classes())
  }

  async ownClasses() {
    return this.cache.fetch('ownClasses', () => this.agent.ownClasses())
  }

  async inspect({ clz }) {
    const func = this.agent.inspect.bind(this.agent, clz)
    return this.cache.fetch(`inspect_${clz}`, func)
  }

  async info() {
    return retry(async() => {
      const sec = await this.agent.checksec()
      const info = await this.agent.info()
      return { sec, info }
    })
  }

  async userDefaults() {
    return this.agent.userDefaults()
  }

  async imports(data) {
    const name = (data && data.name) ? data.name : null
    return this.agent.imports(name)
  }

  async ls({ pathName, root }) {
    return this.agent.ls(pathName, root)
  }

  async plist(fileName) {
    return this.agent.plist(fileName)
  }

  async text(fileName) {
    return this.agent.text(fileName)
  }

  async tables(fileName) {
    return this.agent.tables(fileName)
  }

  async data(arg) {
    return this.agent.data(arg)
  }

  async download(fileName) {
    return this.agent.download(fileName)
  }

  async dumpWindow() {
    return this.agent.dumpWindow()
  }

  async toggleDebugOverlay() {
    return this.agent.toggleDebugOverlay()
  }

  async dumpKeyChain() {
    return this.agent.dumpKeyChain()
  }

  async cookies() {
    return this.agent.cookies()
  }

  async hook({ module, name, args, ret }) {
    return this.agent.hook(module, name, { args, ret })
  }

  async unhook({ module, name }) {
    return this.agent.unhook(module, name)
  }

  async swizzle({ clazz, method, ret }) {
    return this.agent.swizzle(clazz, method, ret)
  }

  async unswizzle({ clazz, method }) {
    return this.agent.unswizzle(clazz, method)
  }

  async dumpdecrypted(name) {
    return this.agent.dumpdecrypted(name)
  }

  async screenshot() {
    return this.agent.screenshot()
  }
}

class SpringBoardHandler extends Handler {
  constructor(session, socket) {
    super()

    this.session = session
    this.socket = socket
    this.handleMessage()
  }

  async load() {
    const { socket } = this
    const script = await this.getAgent('springboard')
    await script.load()
    script.destroyed.connect(() => {
      socket.emit('script_destroyed')
      socket.disconnect(true)
    })
    script.message.connect((message, data) => {
      if (message.type === 'error') this.handleError(message, data)
    })

    this.script = script
    this.agent = script.exports
  }

  handleMessage() {
    [
      'uiopen',
      'urls',
    ].forEach(event => this.socket.on(event, this.wrap(event)))
  }

  async urls() {
    return this.agent.urls()
  }

  async uiopen(url) {
    return this.agent.uiopen(url)
  }
}

module.exports = {
  RpcHandler,
  SpringBoardHandler,
}
<commit_msg>bugfix: disconnect client when process crashed<commit_after>const fs = require('fs')
const os = require('os')
const path = require('path')
const net = require('net')

const { promisify } = require('util')
const { Transform, Writable } = require('stream')

const socketStream = require('socket.io-stream')

const readFile = promisify(fs.readFile)
const Cache = require('./cache')
const { retry, uuidv4 } = require('./utils')

class DownloadStream extends Transform {
  _transform(chunk, encoding, next) {
    this.push(chunk)
    next()
  }
}

class Handler {
  wrap(key) {
    const method = this[key].bind(this)
    return async(data, ack) => {
      try {
        ack({
          status: 'ok',
          data: await method(data),
        })
      } catch (err) {
        console.error('Uncaught RPC', err.stack || err)
        console.error('method:', key, 'args:', data)
        ack({
          status: 'error',
          error: `${err}`,
        })
      }
    }
  }

  handleError(message, error) {
    console.error('error message from frida')
    console.error(message.stack || message)
    console.error(error)
  }

  async getAgent(name) {
    const prefix = path.join(__dirname, '..', 'agent', `${name}.bundle`)
    if (process.env.NODE_ENV === 'development') {
      const source = await readFile(`${prefix}.js`, 'utf8')
      return this.session.createScript(source)
    }
    const bytes = await readFile(`${prefix}.bin`)
    return this.session.createScriptFromBytes(bytes)
  }
}

class BannerStream extends Writable {
  constructor(opt) {
    super(opt)
    this.socket = opt.socket
  }

  _write(chunk, encoding, callback) {
    this.socket.write(chunk, callback)
  }
}

class RpcHandler extends Handler {
  constructor(session, socket) {
    super()

    this.session = session
    this.socket = socket
    this.stream = socketStream(socket)
    this.cache = new Cache()
    this.transfer = new Map()
    this.userScripts = new Map()
    this.handleMessage()

    this.syslogServer = null
    this.syslogClients = new Set()
    this.syslogFile = path.join(os.tmpdir(), Math.random().toString(16).slice(2))
  }

  async createSysLogServer() {
    const server = net.createServer((client) => {
      const ip = client.address().address.green
      this.syslogClients.add(client)
      console.log(`new client ${ip}`)
      const writable = new BannerStream({ socket: client })
      fs.createReadStream(this.syslogFile).pipe(writable)
      client.on('end', () => {
        console.log(`client ${ip} disconnected`)
        this.syslogClients.delete(client)
      })
    })
    this.syslogServer = server
    return new Promise((resolve, reject) => {
      server.listen({ host: 'localhost', port: 0 }, () => {
        console.log(`nc localhost ${server.address().port}`.yellow)
        resolve()
      }).on('error', reject)
    })
  }

  async load() {
    const { socket } = this
    const script = await this.getAgent('app')
    script.destroyed.connect(() => {
      socket.emit('script_destroyed')
      socket.disconnect(true)
      this.syslogClients.forEach(client => client.close())
    })
    script.message.connect((message, data) => {
      const mapping = {
        send: this.handleSend,
        error: this.handleError,
      }
      const handler = mapping[message.type]
      if (typeof handler === 'function')
        handler.call(this, message, data)
      else
        console.warn(`unhandled message type: ${message.type}`.yellow)
    })

    await script.load()
    this.script = script
    this.agent = script.exports

    await this.createSysLogServer()
  }

  handleSend({ payload }, data) {
    if (payload.subject === 'download') {
      const { event, session } = payload
      if (event === 'start') {
        const stream = new DownloadStream()
        this.transfer.set(session, stream)
      } else if (event === 'end') {
        const stream = this.transfer.get(session)
        stream.end()
      } else if (event === 'data') {
        const stream = this.transfer.get(session)
        stream.write(data)
      }
    } else if (payload.subject === 'syslog') {
      fs.appendFile(this.syslogFile, data, () => {})
      this.syslogClients.forEach(client => client.write(data))
    } else {
      if (data !== null) {
        console.warn('does not support binary protocol yet, message payload:'.yellow)
        console.warn(payload)
      }

      this.socket.emit('console', payload) // forward to browser
    }
  }

  handleMessage() {
    [
      'eval',
      'unload',

      'modules',
      'exports',

      'classes',
      'ownClasses',
      'inspect',

      'info',
      'userDefaults',
      'imports',

      'ls',
      'plist',
      'text',
      'download',

      'tables',
      'data',

      'dumpWindow',
      'toggleDebugOverlay',

      'dumpKeyChain',
      'cookies',

      'hook',
      'unhook',
      'swizzle',
      'unswizzle',

      'dumpdecrypted',
      'screenshot',
    ].forEach(event => this.socket.on(event, this.wrap(event)))

    // handle file transfer
    this.stream.on('download', (stream, args) => {
      const { session } = args
      if (session && this.transfer.has(session)) {
        const source = this.transfer.get(session)
        source.pipe(stream).on('finish', () => this.transfer.delete(session))
      }
    })
  }

  async eval(source) {
    const uuid = uuidv4()
    const { socket } = this
    const script = await this.session.createScript(`
      rpc.exports.bootstrap = function(js) {
        // temp workaround for
        // https://github.com/frida/frida-node/pull/28
        // in case the output goes to server side console instead
        // being sent to frontend
        //
        // this is not a sandbox, do not waste your time on escaping it

        ['log', 'warn', 'error'].forEach(function(level) {
          console[level] = function() {
            send({
              subject: 'console.message',
              level: level,
              args: [].slice.call(arguments)
            });
          };
        });

        // wow, copied from frida-python
        try {
          const result = (1, eval)(js);
          if (result instanceof ArrayBuffer) {
            return result;
          } else {
            var type = (result === null) ? 'null' : typeof result;
            return [type, result];
          }
        } catch (e) {
          return ['error', e instanceof Error ? {
            name: e.name,
            message: e.message,
            stack: e.stack
          } : e + ''];
        }
      }
    `)

    script.destroyed.connect(() => {
      socket.emit('userScript', {
        subject: 'destroyed',
        uuid,
      })
    })

    script.message.connect((message, data) => {
      const { type, payload } = message
      // forward to frontend
      socket.emit('userScript', {
        subject: 'message',
        uuid,
        type,
        payload,
        // binary data is not supported right now
        hasData: data !== null,
      })
    })

    try {
      await script.load()
      const result = await script.exports.bootstrap(source)
      let [type, value] = result
      if (result instanceof Buffer) {
        type = 'arraybuffer'
        value = Buffer.from(result).toString('base64')
      }

      this.userScripts.set(uuid, script)

      if (type === 'error') {
        console.error('Uncaught user frida script', value.stack || value)
        return {
          status: 'failed',
          error: value,
        }
      }

      return {
        status: 'ok',
        uuid,
        type,
        value,
      }
    } catch (error) {
      console.error('Uncaught user frida script', error.stack || error)
      return {
        status: 'failed',
        error,
      }
    }
  }

  async unload(uuid) {
    const script = this.userScripts.get(uuid)
    if (script) {
      this.userScripts.delete(uuid)
      return script.unload()
    }
    throw new Error(`script not found: ${uuid}`)
  }

  async modules() {
    return this.cache.fetch('modules', () => this.agent.modules())
  }

  async exports({ module }) {
    return this.agent.exports(module)
  }

  async classes() {
    return this.cache.fetch('classes', () => this.agent.classes())
  }

  async ownClasses() {
    return this.cache.fetch('ownClasses', () => this.agent.ownClasses())
  }

  async inspect({ clz }) {
    const func = this.agent.inspect.bind(this.agent, clz)
    return this.cache.fetch(`inspect_${clz}`, func)
  }

  async info() {
    return retry(async() => {
      const sec = await this.agent.checksec()
      const info = await this.agent.info()
      return { sec, info }
    })
  }

  async userDefaults() {
    return this.agent.userDefaults()
  }

  async imports(data) {
    const name = (data && data.name) ? data.name : null
    return this.agent.imports(name)
  }

  async ls({ pathName, root }) {
    return this.agent.ls(pathName, root)
  }

  async plist(fileName) {
    return this.agent.plist(fileName)
  }

  async text(fileName) {
    return this.agent.text(fileName)
  }

  async tables(fileName) {
    return this.agent.tables(fileName)
  }

  async data(arg) {
    return this.agent.data(arg)
  }

  async download(fileName) {
    return this.agent.download(fileName)
  }

  async dumpWindow() {
    return this.agent.dumpWindow()
  }

  async toggleDebugOverlay() {
    return this.agent.toggleDebugOverlay()
  }

  async dumpKeyChain() {
    return this.agent.dumpKeyChain()
  }

  async cookies() {
    return this.agent.cookies()
  }

  async hook({ module, name, args, ret }) {
    return this.agent.hook(module, name, { args, ret })
  }

  async unhook({ module, name }) {
    return this.agent.unhook(module, name)
  }

  async swizzle({ clazz, method, ret }) {
    return this.agent.swizzle(clazz, method, ret)
  }

  async unswizzle({ clazz, method }) {
    return this.agent.unswizzle(clazz, method)
  }

  async dumpdecrypted(name) {
    return this.agent.dumpdecrypted(name)
  }

  async screenshot() {
    return this.agent.screenshot()
  }
}

class SpringBoardHandler extends Handler {
  constructor(session, socket) {
    super()

    this.session = session
    this.socket = socket
    this.handleMessage()
  }

  async load() {
    const { socket } = this
    const script = await this.getAgent('springboard')
    await script.load()
    script.destroyed.connect(() => {
      socket.emit('script_destroyed')
      socket.disconnect(true)
    })
    script.message.connect((message, data) => {
      if (message.type === 'error') this.handleError(message, data)
    })

    this.script = script
    this.agent = script.exports
  }

  handleMessage() {
    [
      'uiopen',
      'urls',
    ].forEach(event => this.socket.on(event, this.wrap(event)))
  }

  async urls() {
    return this.agent.urls()
  }

  async uiopen(url) {
    return this.agent.uiopen(url)
  }
}

module.exports = {
  RpcHandler,
  SpringBoardHandler,
}
<|endoftext|>


<commit_before>goog.provide('ol.source.Vector');

goog.require('ol.Attribution');
goog.require('ol.Extent');
goog.require('ol.Feature');
goog.require('ol.Projection');
goog.require('ol.filter.Extent');
goog.require('ol.filter.Filter');
goog.require('ol.filter.Geometry');
goog.require('ol.filter.Logical');
goog.require('ol.geom.GeometryType');
goog.require('ol.source.TileSource');
goog.require('ol.structs.RTree');
goog.require('ol.tilegrid.TileGrid');

/**
 * @constructor
 */
ol.source.FeatureCache = function() {

  /**
   * @type {Object.<string, ol.Feature>}
   * @private
   */
  this.idLookup_;

  /**
   * @type {Object.<string, ol.Feature>}
   * @private
   */
  this.geometryTypeIndex_;

  /**
   * @type {Object.<ol.geom.GeometryType, ol.structs.RTree>}
   * @private
   */
  this.boundsByGeometryType_;

  this.clear();

};

/**
 * Clear the cache.
 */
ol.source.FeatureCache.prototype.clear = function() {
  this.idLookup_ = {};

  var geometryTypeIndex = {},
      boundsByGeometryType = {},
      geometryType;
  for (var key in ol.geom.GeometryType) {
    geometryType = ol.geom.GeometryType[key];
    geometryTypeIndex[geometryType] = {};
    boundsByGeometryType[geometryType] = new ol.structs.RTree();
  }
  this.geometryTypeIndex_ = geometryTypeIndex;
  this.boundsByGeometryType_ = boundsByGeometryType;
};

/**
 * Add a feature to the cache.
 * @param {ol.Feature} feature Feature to be cached.
 */
ol.source.FeatureCache.prototype.add = function(feature) {
  var id = goog.getUid(feature).toString(),
      geometry = feature.getGeometry();

  this.idLookup_[id] = feature;

  // index by geometry type and bounding box
  if (!goog.isNull(geometry)) {
    var geometryType = geometry.getType();
    this.geometryTypeIndex_[geometryType][id] = feature;
    this.boundsByGeometryType_[geometryType].put(geometry.getBounds(),
        feature);
  }
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Object.<string, ol.Feature>} Object of features, keyed by id.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesObject_ = function(opt_filter) {
  var i, features;
  if (!goog.isDef(opt_filter)) {
    features = this.idLookup_;
  } else {
    if (opt_filter instanceof ol.filter.Geometry) {
      features = this.geometryTypeIndex_[opt_filter.getType()];
    } else if (opt_filter instanceof ol.filter.Extent) {
      var boundsByGeometryType = this.boundsByGeometryType_,
          extent = opt_filter.getExtent();
      features = {};
      for (i in boundsByGeometryType) {
        goog.object.extend(features, boundsByGeometryType[i].find(extent));
      }
    } else if (opt_filter instanceof ol.filter.Logical) {
      var filters = opt_filter.getFilters();
      if (filters.length === 2) {
        var filter, geometryFilter, extentFilter;
        for (i = 0; i <= 1; ++i) {
          filter = filters[i];
          if (filter instanceof ol.filter.Geometry) {
            geometryFilter = filter;
          } else if (filter instanceof ol.filter.Extent) {
            extentFilter = filter;
          }
        }
        if (extentFilter && geometryFilter) {
          features = this.boundsByGeometryType_[geometryFilter.getType()]
              .find(extentFilter.getExtent());
        }
      }
    }
    if (!goog.isDef(features)) {
      // TODO: support fast lane for other filter types
      var candidates = this.idLookup_,
          feature;
      features = {};
      for (i in candidates) {
        feature = candidates[i];
        if (opt_filter.evaluate(feature) === true) {
          features[i] = feature;
        }
      }
    }
  }
  return features;
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Array.<ol.Feature>} Array of features.
 */
ol.source.FeatureCache.prototype.getFeatures = function(opt_filter) {
  return goog.object.getValues(this.getFeaturesObject_(opt_filter));
};

/**
 * @param {ol.filter.Geometry} filter Geometry type filter.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesByGeometryType_ = function(filter) {
  return goog.object.getValues(this.geometryTypeIndex_[filter.getType()]);
};

/**
 * Get features by ids.
 * @param {Array.<string>} ids Array of (internal) identifiers.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesByIds_ = function(ids) {
  var len = ids.length,
      features = new Array(len),
      i;
  for (i = 0; i < len; ++i) {
    features[i] = this.idLookup_[ids[i]];
  }
  return features;
};

/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            extent: (ol.Extent|undefined),
 *            projection: (ol.Projection|undefined),
 *            tileGrid: (ol.tilegrid.TileGrid|undefined)}}
 */
ol.source.VectorOptions;

/**
 * @constructor
 * @extends {ol.source.TileSource}
 * @param {ol.source.VectorOptions} options Source options.
 */
ol.source.Vector = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    projection: options.projection,
    tileGrid: options.tileGrid
  });

  /**
   * @type {ol.source.FeatureCache}
   * @private
   */
  this.featureCache_ = new ol.source.FeatureCache();

};
goog.inherits(ol.source.Vector, ol.source.TileSource);

/**
 * @param {ol.tilegrid.TileGrid} tileGrid tile grid.
 */
ol.source.Vector.prototype.setTileGrid = function(tileGrid) {
  this.tileGrid = tileGrid;
};

/**
 * @param {Array.<ol.Feature>} features Array of features.
 */
ol.source.Vector.prototype.addFeatures = function(features) {
  for (var i = 0, ii = features.length; i < ii; ++i) {
    this.featureCache_.add(features[i]);
  }
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Array.<ol.Feature>} Array of features.
 */
ol.source.Vector.prototype.getFeatures = function(opt_filter) {
  return this.featureCache_.getFeatures(opt_filter);
};
<commit_msg>Only attempt fast lane for AND filters<commit_after>goog.provide('ol.source.Vector');

goog.require('ol.Attribution');
goog.require('ol.Extent');
goog.require('ol.Feature');
goog.require('ol.Projection');
goog.require('ol.filter.Extent');
goog.require('ol.filter.Filter');
goog.require('ol.filter.Geometry');
goog.require('ol.filter.Logical');
goog.require('ol.geom.GeometryType');
goog.require('ol.source.TileSource');
goog.require('ol.structs.RTree');
goog.require('ol.tilegrid.TileGrid');

/**
 * @constructor
 */
ol.source.FeatureCache = function() {

  /**
   * @type {Object.<string, ol.Feature>}
   * @private
   */
  this.idLookup_;

  /**
   * @type {Object.<string, ol.Feature>}
   * @private
   */
  this.geometryTypeIndex_;

  /**
   * @type {Object.<ol.geom.GeometryType, ol.structs.RTree>}
   * @private
   */
  this.boundsByGeometryType_;

  this.clear();

};

/**
 * Clear the cache.
 */
ol.source.FeatureCache.prototype.clear = function() {
  this.idLookup_ = {};

  var geometryTypeIndex = {},
      boundsByGeometryType = {},
      geometryType;
  for (var key in ol.geom.GeometryType) {
    geometryType = ol.geom.GeometryType[key];
    geometryTypeIndex[geometryType] = {};
    boundsByGeometryType[geometryType] = new ol.structs.RTree();
  }
  this.geometryTypeIndex_ = geometryTypeIndex;
  this.boundsByGeometryType_ = boundsByGeometryType;
};

/**
 * Add a feature to the cache.
 * @param {ol.Feature} feature Feature to be cached.
 */
ol.source.FeatureCache.prototype.add = function(feature) {
  var id = goog.getUid(feature).toString(),
      geometry = feature.getGeometry();

  this.idLookup_[id] = feature;

  // index by geometry type and bounding box
  if (!goog.isNull(geometry)) {
    var geometryType = geometry.getType();
    this.geometryTypeIndex_[geometryType][id] = feature;
    this.boundsByGeometryType_[geometryType].put(geometry.getBounds(),
        feature);
  }
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Object.<string, ol.Feature>} Object of features, keyed by id.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesObject_ = function(opt_filter) {
  var i, features;
  if (!goog.isDef(opt_filter)) {
    features = this.idLookup_;
  } else {
    if (opt_filter instanceof ol.filter.Geometry) {
      features = this.geometryTypeIndex_[opt_filter.getType()];
    } else if (opt_filter instanceof ol.filter.Extent) {
      var boundsByGeometryType = this.boundsByGeometryType_,
          extent = opt_filter.getExtent();
      features = {};
      for (i in boundsByGeometryType) {
        goog.object.extend(features, boundsByGeometryType[i].find(extent));
      }
    } else if (opt_filter instanceof ol.filter.Logical &&
        opt_filter.operator === ol.filter.LogicalOperator.AND) {
      var filters = opt_filter.getFilters();
      if (filters.length === 2) {
        var filter, geometryFilter, extentFilter;
        for (i = 0; i <= 1; ++i) {
          filter = filters[i];
          if (filter instanceof ol.filter.Geometry) {
            geometryFilter = filter;
          } else if (filter instanceof ol.filter.Extent) {
            extentFilter = filter;
          }
        }
        if (extentFilter && geometryFilter) {
          features = this.boundsByGeometryType_[geometryFilter.getType()]
              .find(extentFilter.getExtent());
        }
      }
    }
    if (!goog.isDef(features)) {
      // TODO: support fast lane for other filter types
      var candidates = this.idLookup_,
          feature;
      features = {};
      for (i in candidates) {
        feature = candidates[i];
        if (opt_filter.evaluate(feature) === true) {
          features[i] = feature;
        }
      }
    }
  }
  return features;
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Array.<ol.Feature>} Array of features.
 */
ol.source.FeatureCache.prototype.getFeatures = function(opt_filter) {
  return goog.object.getValues(this.getFeaturesObject_(opt_filter));
};

/**
 * @param {ol.filter.Geometry} filter Geometry type filter.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesByGeometryType_ = function(filter) {
  return goog.object.getValues(this.geometryTypeIndex_[filter.getType()]);
};

/**
 * Get features by ids.
 * @param {Array.<string>} ids Array of (internal) identifiers.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.source.FeatureCache.prototype.getFeaturesByIds_ = function(ids) {
  var len = ids.length,
      features = new Array(len),
      i;
  for (i = 0; i < len; ++i) {
    features[i] = this.idLookup_[ids[i]];
  }
  return features;
};

/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            extent: (ol.Extent|undefined),
 *            projection: (ol.Projection|undefined),
 *            tileGrid: (ol.tilegrid.TileGrid|undefined)}}
 */
ol.source.VectorOptions;

/**
 * @constructor
 * @extends {ol.source.TileSource}
 * @param {ol.source.VectorOptions} options Source options.
 */
ol.source.Vector = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    projection: options.projection,
    tileGrid: options.tileGrid
  });

  /**
   * @type {ol.source.FeatureCache}
   * @private
   */
  this.featureCache_ = new ol.source.FeatureCache();

};
goog.inherits(ol.source.Vector, ol.source.TileSource);

/**
 * @param {ol.tilegrid.TileGrid} tileGrid tile grid.
 */
ol.source.Vector.prototype.setTileGrid = function(tileGrid) {
  this.tileGrid = tileGrid;
};

/**
 * @param {Array.<ol.Feature>} features Array of features.
 */
ol.source.Vector.prototype.addFeatures = function(features) {
  for (var i = 0, ii = features.length; i < ii; ++i) {
    this.featureCache_.add(features[i]);
  }
};

/**
 * @param {ol.filter.Filter=} opt_filter Optional filter.
 * @return {Array.<ol.Feature>} Array of features.
 */
ol.source.Vector.prototype.getFeatures = function(opt_filter) {
  return this.featureCache_.getFeatures(opt_filter);
};
<|endoftext|>


<commit_before>const Zabbix = require('../index');

const whiteSpaceCount = 2;
const timeFrom = Date.parse('Mar 1 2017 00:00:00 GMT') / 1000;
const timeTill = Date.parse('Mar 31 2017 23:59:59 GMT') / 1000;
const event = {}; // eslint: will contain ack and unack event count
const getEventParams = {
  'countOutput': true,
  'time_from': timeFrom,
  'time_till': timeTill,
  'acknowledged': false,
  'source': 0, // eslint: event created by a trigger
  'value': 1 // eslint: problem events only
};

const zabbix = new Zabbix(
  'https://zabbix/zabbix/api_jsonrpc.php',
  process.env.ZABBIX_USER,
  process.env.ZABBIX_PASSWORD,
  false // eslint: https rejectUnauthorized boolean
);

zabbix.login()
  .then(() => zabbix.request('event.get', getEventParams))
  .then((value) => {
    event.unack = value;
    getEventParams.acknowledged = true;

    return zabbix.request('event.get', getEventParams);
  })
  .then((value) => {
    event.ack = value;
    console.log(JSON.stringify(event, null, whiteSpaceCount));
  })
  .then(() => zabbix.logout())
  .catch((reson) => console.log(JSON.stringify(reson, null, whiteSpaceCount)));
<commit_msg>Closes #13<commit_after>const Zabbix = require('../index');

const whiteSpaceCount = 2;
const timeFrom = Date.parse('Mar 1 2017 00:00:00 GMT') / 1000;
const timeTill = Date.parse('Mar 31 2017 23:59:59 GMT') / 1000;
const event = {}; // eslint: will contain ack and unack event count
const getEventParams = {
  'countOutput': true,
  'time_from': timeFrom,
  'time_till': timeTill,
  'acknowledged': false,
  'source': 0, // eslint: event created by a trigger
  'value': 1 // eslint: problem events only
};

const zabbix = new Zabbix(
  'https://zabbix/zabbix/api_jsonrpc.php',
  process.env.ZABBIX_USER,
  process.env.ZABBIX_PASSWORD,
  false // eslint: https rejectUnauthorized boolean
);

zabbix.login()
  .then(() => zabbix.request('event.get', getEventParams))
  .then((value) => {
    event.unack = value;
    getEventParams.acknowledged = true;

    return zabbix.request('event.get', getEventParams);
  })
  .then((value) => {
    event.ack = value;
    console.log(JSON.stringify(event, null, whiteSpaceCount));
    return value;
  })
  .then(() => zabbix.logout())
  .catch((reson) => console.log(JSON.stringify(reson, null, whiteSpaceCount)));
<|endoftext|>


<commit_before>!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=x.elements;return"string"==typeof a?a.split(" "):a}function e(a){var b=w[a[u]];return b||(b={},v++,a[u]=v,w[v]=b),b}function f(a,c,d){if(c||(c=b),p)return c.createElement(a);d||(d=e(c));var f;return f=d.cache[a]?d.cache[a].cloneNode():t.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),f.canHaveChildren&&!s.test(a)?d.frag.appendChild(f):f}function g(a,c){if(a||(a=b),p)return a.createDocumentFragment();c=c||e(a);for(var f=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)f.createElement(h[g]);return f}function h(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return x.shivMethods?f(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/\w+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(x,b.frag)}function i(a){a||(a=b);var d=e(a);return!x.shivCSS||o||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}")),p||h(a,d),a}function j(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(k(b)));return g}function k(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(z+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function l(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+z+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function m(a){for(var b=a.length;b--;)a[b].removeNode()}function n(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,f,g=e(a),h=a.namespaces,i=a.parentWindow;return!A||a.printShived?a:("undefined"==typeof h[z]&&h.add(z),i.attachEvent("onbeforeprint",function(){b();for(var e,g,h,i=a.styleSheets,k=[],m=i.length,n=Array(m);m--;)n[m]=i[m];for(;h=n.pop();)if(!h.disabled&&y.test(h.media)){try{e=h.imports,g=e.length}catch(o){g=0}for(m=0;g>m;m++)n.push(e[m]);try{k.push(h.cssText)}catch(o){}}k=l(k.reverse().join("")),f=j(a),d=c(a,k)}),i.attachEvent("onafterprint",function(){m(f),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var o,p,q="3.6.2pre",r=a.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,t=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,u="_html5shiv",v=0,w={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",o="hidden"in a,p=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){o=!0,p=!0}}();var x={elements:r.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup main mark meter nav output progress section summary time video",version:q,shivCSS:r.shivCSS!==!1,supportsUnknownElements:p,shivMethods:r.shivMethods!==!1,type:"default",shivDocument:i,createElement:f,createDocumentFragment:g};a.html5=x,i(b);var y=/^$|\b(?:all|print)\b/,z="html5shiv",A=!p&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();x.type+=" print",x.shivPrint=n,n(b)}(this,document);<commit_msg>update HTML5 shiv for <picture><commit_after>/**
* @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.2",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="<xyz></xyz>",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b)}(this,document);<|endoftext|>


<commit_before>Minionette.CollectionView = Minionette.View.extend({
    constructor: function(options) {
        // Initialize a object for our modelViews
        this._modelViews = {};
        // Ensure this has a ModelView to initialize
        // new modelViews from.
        this._ensureModelView(options || {});

        Minionette.View.apply(this, arguments);

        // Make sure we remove our modelViews when this is removed.
        this.listenTo(this, 'remove', this._removeModelViews);
    },

    // Listen to the default events.
    collectionEvents: {
        'add': 'addOne',
        'remove': 'removeOne',
        'reset': 'render',
        'sort': 'render'
    },

    // A default useful render function.
    render: function() {
        this.trigger('render');

        // Dump all our modelViews.
        this._removeModelViews();

        var $el = this.$el.html(this.template(this._serialize()));
        // Use a DocumentFragment to speed up #render()
        this.$el = $(document.createDocumentFragment());

        // Loop through all our models, and build their view.
        this.collection.each(this._addModelView, this);

        // Append the DocumentFragment to the rendered template,
        // and set that as this.$el
        this.$el = $el.append(this.$el);

        return this;
    },

    // Add an individual model's view to this.$el.
    addOne: function(model) {
        this.trigger('addOne');

        var view = this._addModelView(model);

        return view;
    },

    // Remove an individual model's view from this.$el.
    removeOne: function(model) {
        this.trigger('removeOne');

        var view = _.findWhere(this._modelViews, {model: model});
        if (view) { view.remove(); }

        return view;
    },

    // Add an individual model's view to this.$el.
    _addModelView: function(model) {
        var modelView = new this.ModelView({model: model});

        // Add the modelView, and keep track of it.
        this._modelViews[modelView.cid] = modelView;
        modelView._parent = this;

        this.$el.append(modelView.render().$el);
        return modelView;
    },

    // A hook method that is called during
    // a view#remove().
    _removeView: function(view) {
        delete this._modelViews[view.cid];
    },

    // A callback method bound to the 'remove:before'
    // event. Removes all our modelViews.
    _removeModelViews: function() {
        _.invoke(this._modelViews, 'remove');
    },

    // Sets this.ModelView. Prioritizes instantiated options.ModelView,
    // then a subclass' prototype ModelView, and defaults to Minionette.ModelView
    _ensureModelView: function(options) {
        this.ModelView = options.ModelView || this.ModelView || Minionette.ModelView;
    }
});
<commit_msg>Refactor CollectionView<commit_after>Minionette.CollectionView = Minionette.View.extend({
    constructor: function(options) {
        // Initialize a object for our modelViews
        this._modelViews = {};
        // Ensure this has a ModelView to initialize
        // new modelViews from.
        this._ensureModelView(options || {});

        Minionette.View.apply(this, arguments);

        // Make sure we remove our modelViews when this is removed.
        this.listenTo(this, 'remove', this._removeModelViews);
    },

    // Listen to the default events.
    collectionEvents: {
        'add': 'addOne',
        'remove': 'removeOne',
        'reset': 'render',
        'sort': 'render'
    },

    // A default useful render function.
    render: function() {
        this.trigger('render');

        // Dump all our modelViews.
        this._removeModelViews();

        var $el = this.$el.html(this.template(this._serialize()));
        // Use a DocumentFragment to speed up #render()
        this.$el = $(document.createDocumentFragment());

        // Loop through all our models, and build their view.
        this.collection.each(this.addOne, this);

        // Append the DocumentFragment to the rendered template,
        // and set that as this.$el
        this.$el = $el.append(this.$el);

        return this;
    },

    // Add an individual model's view to this.$el.
    addOne: function(model) {
        var view = new this.ModelView({model: model});

        // Add the modelView, and keep track of it.
        this._modelViews[view.cid] = view;
        view._parent = this;

        this.trigger('addOne', view);

        this.$el.append(view.render().$el);
        return view;
    },

    // Remove an individual model's view from this.$el.
    removeOne: function(model) {
        // This may or may not find a view.
        var view = _.findWhere(this._modelViews, {model: model});

        this.trigger('removeOne', view);
        attempt(view, 'remove');

        return view;
    },

    // A hook method that is called during
    // a view#remove().
    _removeView: function(view) {
        delete this._modelViews[view.cid];
    },

    // A callback method bound to the 'remove:before'
    // event. Removes all our modelViews.
    _removeModelViews: function() {
        _.invoke(this._modelViews, 'remove');
    },

    // Sets this.ModelView. Prioritizes instantiated options.ModelView,
    // then a subclass' prototype ModelView, and defaults to Minionette.ModelView
    _ensureModelView: function(options) {
        this.ModelView = options.ModelView || this.ModelView || Minionette.ModelView;
    }
});
<|endoftext|>


<commit_before>function getAnalyticsMetadata() {
    const analyticsTag = document.querySelector('meta[name=ghost-analytics-id]');
    const analyticsId = analyticsTag?.content;
    if (analyticsTag) {
        return {
            entry_id: analyticsId,
            source_url: window.location.href
        };
    }
    return null;
}

function setupGhostApi({siteUrl = window.location.origin}) {
    const apiPath = 'members/api';

    function endpointFor({type, resource}) {
        if (type === 'members') {
            return `${siteUrl.replace(/\/$/, '')}/${apiPath}/${resource}/`;
        }
    }

    function makeRequest({url, method = 'GET', headers = {}, credentials = undefined, body = undefined}) {
        const options = {
            method,
            headers,
            credentials,
            body
        };
        return fetch(url, options);
    }
    const api = {};

    api.analytics = {
        async pushEvent(event) {
            const url = endpointFor({type: 'members', resource: 'events'});
            const identity = await api.member.identity();
            event.identity = identity;
            const body = {
                events: [event]
            };
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (!res.ok) {
                    return null;
                }
                return res.text();
            });
        }
    };

    api.site = {
        read() {
            const url = endpointFor({type: 'members', resource: 'site'});
            return makeRequest({
                url,
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (res) {
                if (res.ok) {
                    return res.json();
                } else {
                    throw new Error('Failed to fetch site data');
                }
            });
        },

        offer({offerId}) {
            const url = endpointFor({type: 'members', resource: 'offers'}) + offerId + '/';
            return makeRequest({
                url,
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (res) {
                if (res.ok) {
                    return res.json();
                } else {
                    throw new Error('Failed to fetch offer data');
                }
            });
        }
    };

    api.member = {
        identity() {
            const url = endpointFor({type: 'members', resource: 'session'});
            return makeRequest({
                url,
                credentials: 'same-origin'
            }).then(function (res) {
                if (!res.ok || res.status === 204) {
                    return null;
                }
                return res.text();
            });
        },

        sessionData() {
            const url = endpointFor({type: 'members', resource: 'member'});
            return makeRequest({
                url,
                credentials: 'same-origin'
            }).then(function (res) {
                if (!res.ok || res.status === 204) {
                    return null;
                }
                return res.json();
            });
        },

        update({name, subscribed}) {
            const url = endpointFor({type: 'members', resource: 'member'});
            const body = {
                name,
                subscribed
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                credentials: 'same-origin',
                body: JSON.stringify(body)
            }).then(function (res) {
                if (!res.ok) {
                    return null;
                }
                return res.json();
            });
        },

        sendMagicLink({email, emailType, labels, name, oldEmail}) {
            const url = endpointFor({type: 'members', resource: 'send-magic-link'});
            const body = {
                name,
                email,
                oldEmail,
                emailType,
                labels,
                requestSrc: 'portal'
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (res.ok) {
                    return 'Success';
                } else {
                    throw new Error('Failed to send magic link email');
                }
            });
        },

        signout() {
            const url = endpointFor({type: 'members', resource: 'session'});
            return makeRequest({
                url,
                method: 'DELETE'
            }).then(function (res) {
                if (res.ok) {
                    window.location.replace(siteUrl);
                    return 'Success';
                } else {
                    throw new Error('Failed to signout');
                }
            });
        },

        async updateEmailAddress({email}) {
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'member/email'});
            const body = {
                email,
                identity
            };

            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (res.ok) {
                    return 'Success';
                } else {
                    throw new Error('Failed to send email address verification email');
                }
            });
        },

        async checkoutPlan({plan, cancelUrl, successUrl, email: customerEmail, name, offerId, metadata = {}} = {}) {
            const siteUrlObj = new URL(siteUrl);
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});
            if (!successUrl) {
                const checkoutSuccessUrl = new URL(siteUrl);
                checkoutSuccessUrl.searchParams.set('stripe', 'success');
                successUrl = checkoutSuccessUrl.href;
            }

            if (!cancelUrl) {
                const checkoutCancelUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
                checkoutCancelUrl.searchParams.set('stripe', 'cancel');
                cancelUrl = checkoutCancelUrl.href;
            }
            const metadataObj = {
                name,
                requestSrc: 'portal',
                fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,
                ...metadata
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                metadataObj.ghost_analytics_entry_id = analyticsData.entry_id;
                metadataObj.ghost_analytics_source_url = analyticsData.source_url;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    priceId: offerId ? null : plan,
                    offerId,
                    identity: identity,
                    metadata: metadataObj,
                    successUrl,
                    cancelUrl,
                    customerEmail: customerEmail
                })
            }).then(function (res) {
                if (!res.ok) {
                    throw new Error('Could not create stripe checkout session');
                }
                return res.json();
            }).then(function (result) {
                const stripe = window.Stripe(result.publicKey);
                return stripe.redirectToCheckout({
                    sessionId: result.sessionId
                });
            }).then(function (result) {
                if (result.error) {
                    throw new Error(result.error.message);
                }
            }).catch(function (err) {
                throw err;
            });
        },

        async editBilling({successUrl, cancelUrl, subscriptionId} = {}) {
            const siteUrlObj = new URL(siteUrl);
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'create-stripe-update-session'});
            if (!successUrl) {
                const checkoutSuccessUrl = new URL(siteUrl);
                checkoutSuccessUrl.searchParams.set('stripe', 'billing-update-success');
                successUrl = checkoutSuccessUrl.href;
            }

            if (!cancelUrl) {
                const checkoutCancelUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
                checkoutCancelUrl.searchParams.set('stripe', 'billing-update-cancel');
                cancelUrl = checkoutCancelUrl.href;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    identity: identity,
                    subscription_id: subscriptionId,
                    successUrl,
                    cancelUrl
                })
            }).then(function (res) {
                if (!res.ok) {
                    throw new Error('Could not create stripe checkout session');
                }
                return res.json();
            }).then(function (result) {
                const stripe = window.Stripe(result.publicKey);
                return stripe.redirectToCheckout({
                    sessionId: result.sessionId
                });
            }).then(function (result) {
                if (result.error) {
                    throw new Error(result.error.message);
                }
            }).catch(function (err) {
                throw err;
            });
        },

        async updateSubscription({subscriptionId, planName, planId, smartCancel, cancelAtPeriodEnd, cancellationReason}) {
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/';
            const body = {
                smart_cancel: smartCancel,
                cancel_at_period_end: cancelAtPeriodEnd,
                cancellation_reason: cancellationReason,
                identity: identity,
                priceId: planId
            };
            const analyticsData = getAnalyticsMetadata();
            if (body) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            });
        }
    };

    api.init = async () => {
        const [{site}, member] = await Promise.all([
            api.site.read(),
            api.member.sessionData()
        ]);
        return {site, member};
    };

    return api;
}

export default setupGhostApi;
<commit_msg>Removed logic to set default urls for checkout<commit_after>function getAnalyticsMetadata() {
    const analyticsTag = document.querySelector('meta[name=ghost-analytics-id]');
    const analyticsId = analyticsTag?.content;
    if (analyticsTag) {
        return {
            entry_id: analyticsId,
            source_url: window.location.href
        };
    }
    return null;
}

function setupGhostApi({siteUrl = window.location.origin}) {
    const apiPath = 'members/api';

    function endpointFor({type, resource}) {
        if (type === 'members') {
            return `${siteUrl.replace(/\/$/, '')}/${apiPath}/${resource}/`;
        }
    }

    function makeRequest({url, method = 'GET', headers = {}, credentials = undefined, body = undefined}) {
        const options = {
            method,
            headers,
            credentials,
            body
        };
        return fetch(url, options);
    }
    const api = {};

    api.analytics = {
        async pushEvent(event) {
            const url = endpointFor({type: 'members', resource: 'events'});
            const identity = await api.member.identity();
            event.identity = identity;
            const body = {
                events: [event]
            };
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (!res.ok) {
                    return null;
                }
                return res.text();
            });
        }
    };

    api.site = {
        read() {
            const url = endpointFor({type: 'members', resource: 'site'});
            return makeRequest({
                url,
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (res) {
                if (res.ok) {
                    return res.json();
                } else {
                    throw new Error('Failed to fetch site data');
                }
            });
        },

        offer({offerId}) {
            const url = endpointFor({type: 'members', resource: 'offers'}) + offerId + '/';
            return makeRequest({
                url,
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (res) {
                if (res.ok) {
                    return res.json();
                } else {
                    throw new Error('Failed to fetch offer data');
                }
            });
        }
    };

    api.member = {
        identity() {
            const url = endpointFor({type: 'members', resource: 'session'});
            return makeRequest({
                url,
                credentials: 'same-origin'
            }).then(function (res) {
                if (!res.ok || res.status === 204) {
                    return null;
                }
                return res.text();
            });
        },

        sessionData() {
            const url = endpointFor({type: 'members', resource: 'member'});
            return makeRequest({
                url,
                credentials: 'same-origin'
            }).then(function (res) {
                if (!res.ok || res.status === 204) {
                    return null;
                }
                return res.json();
            });
        },

        update({name, subscribed}) {
            const url = endpointFor({type: 'members', resource: 'member'});
            const body = {
                name,
                subscribed
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                credentials: 'same-origin',
                body: JSON.stringify(body)
            }).then(function (res) {
                if (!res.ok) {
                    return null;
                }
                return res.json();
            });
        },

        sendMagicLink({email, emailType, labels, name, oldEmail}) {
            const url = endpointFor({type: 'members', resource: 'send-magic-link'});
            const body = {
                name,
                email,
                oldEmail,
                emailType,
                labels,
                requestSrc: 'portal'
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (res.ok) {
                    return 'Success';
                } else {
                    throw new Error('Failed to send magic link email');
                }
            });
        },

        signout() {
            const url = endpointFor({type: 'members', resource: 'session'});
            return makeRequest({
                url,
                method: 'DELETE'
            }).then(function (res) {
                if (res.ok) {
                    window.location.replace(siteUrl);
                    return 'Success';
                } else {
                    throw new Error('Failed to signout');
                }
            });
        },

        async updateEmailAddress({email}) {
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'member/email'});
            const body = {
                email,
                identity
            };

            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }).then(function (res) {
                if (res.ok) {
                    return 'Success';
                } else {
                    throw new Error('Failed to send email address verification email');
                }
            });
        },

        async checkoutPlan({plan, cancelUrl, successUrl, email: customerEmail, name, offerId, metadata = {}} = {}) {
            const siteUrlObj = new URL(siteUrl);
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});

            if (!cancelUrl) {
                const checkoutCancelUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
                checkoutCancelUrl.searchParams.set('stripe', 'cancel');
                cancelUrl = checkoutCancelUrl.href;
            }
            const metadataObj = {
                name,
                requestSrc: 'portal',
                fp_tid: (window.FPROM || window.$FPROM)?.data?.tid,
                ...metadata
            };
            const analyticsData = getAnalyticsMetadata();
            if (analyticsData) {
                metadataObj.ghost_analytics_entry_id = analyticsData.entry_id;
                metadataObj.ghost_analytics_source_url = analyticsData.source_url;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    priceId: offerId ? null : plan,
                    offerId,
                    identity: identity,
                    metadata: metadataObj,
                    successUrl,
                    cancelUrl,
                    customerEmail: customerEmail
                })
            }).then(function (res) {
                if (!res.ok) {
                    throw new Error('Could not create stripe checkout session');
                }
                return res.json();
            }).then(function (result) {
                const stripe = window.Stripe(result.publicKey);
                return stripe.redirectToCheckout({
                    sessionId: result.sessionId
                });
            }).then(function (result) {
                if (result.error) {
                    throw new Error(result.error.message);
                }
            }).catch(function (err) {
                throw err;
            });
        },

        async editBilling({successUrl, cancelUrl, subscriptionId} = {}) {
            const siteUrlObj = new URL(siteUrl);
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'create-stripe-update-session'});
            if (!successUrl) {
                const checkoutSuccessUrl = new URL(siteUrl);
                checkoutSuccessUrl.searchParams.set('stripe', 'billing-update-success');
                successUrl = checkoutSuccessUrl.href;
            }

            if (!cancelUrl) {
                const checkoutCancelUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
                checkoutCancelUrl.searchParams.set('stripe', 'billing-update-cancel');
                cancelUrl = checkoutCancelUrl.href;
            }
            return makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    identity: identity,
                    subscription_id: subscriptionId,
                    successUrl,
                    cancelUrl
                })
            }).then(function (res) {
                if (!res.ok) {
                    throw new Error('Could not create stripe checkout session');
                }
                return res.json();
            }).then(function (result) {
                const stripe = window.Stripe(result.publicKey);
                return stripe.redirectToCheckout({
                    sessionId: result.sessionId
                });
            }).then(function (result) {
                if (result.error) {
                    throw new Error(result.error.message);
                }
            }).catch(function (err) {
                throw err;
            });
        },

        async updateSubscription({subscriptionId, planName, planId, smartCancel, cancelAtPeriodEnd, cancellationReason}) {
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'subscriptions'}) + subscriptionId + '/';
            const body = {
                smart_cancel: smartCancel,
                cancel_at_period_end: cancelAtPeriodEnd,
                cancellation_reason: cancellationReason,
                identity: identity,
                priceId: planId
            };
            const analyticsData = getAnalyticsMetadata();
            if (body) {
                body.metadata = analyticsData;
            }
            return makeRequest({
                url,
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            });
        }
    };

    api.init = async () => {
        const [{site}, member] = await Promise.all([
            api.site.read(),
            api.member.sessionData()
        ]);
        return {site, member};
    };

    return api;
}

export default setupGhostApi;
<|endoftext|>


<commit_before>require('babel/register')

var path = require('path')
var fs = require('fs-extra')
var assign = require('object-assign')
var browserify = require('browserify')
var babelify = require('babelify')
var glob = require('glob')
var mustache = require('mustache')
var reactDocGen = require('react-docgen')
var watchify = require('watchify')

/**
 * React Styleguide Generator
 *
 * @class
 * @param {string} input
 * @param {Object} opts
 * @param {string=} opts.output
 * @param {string=} opts.title
 * @param {string=|Object} opts.config
 * @param {string=} opts.root
 * @param {boolean=} opts.pushstate
 * @param {string[]=} opts.files
 * @param {Object=} opts.babelConfig
 * @param {Object=} opts.browserifyConfig
 * @param {Object=} opts.reactDocgen
 * @param {Object=} opts.typekit
 */
function RSG (input, opts) {
  opts = opts || {}

  this.log = require('./logger')('rsg-lib', { debug: opts.verbose || false })

  var config

  // If feeding in a direct config object
  if (opts.config !== null && typeof opts.config === 'object') {
    config = opts.config
  } else {
    config = this.readConfig(opts.config)
  }

  opts = assign(config, opts)

  opts.output = path.resolve(process.cwd(), (opts.output || 'styleguide').replace(/\/+$/, ''))
  opts.title = opts.title || 'Style Guide'
  opts.root = opts.root ? path.normalize('/' + opts.root.replace(/\/+$/, '')) : null
  opts.pushstate = opts.pushstate || false
  opts.files = opts.files || []
  opts.babelConfig = opts.babelConfig || { stage: 0 }
  opts.browserifyConfig = assign({ debug: true }, opts.browserifyConfig, {
    cache: {},
    packageCache: {},
    standalone: 'Contents'
  })

  this.input = glob.sync(input, { realpath: true })

  opts['reactDocgen'] = opts['reactDocgen'] || {}

  if (!opts['reactDocgen'].files) {
    opts['reactDocgen'].files = [input]
  }

  this.opts = opts
  this.reactVersion = require('react').version

  // stores prop documentation generated from react-docgen
  this.reactPropMetas = {}

  // files to parse for react-docgen
  this.reactDocGenFiles = this.getReactPropDocFiles()

  // Cache of file modification times so we're not re-parsing
  // files that have not changed in react-docgen for perf
  this.reactDocGenCache = {}

  // has the first-pass of parsing happened?
  this.reactDocGenInit = false

  // Cached files to include into the styleguide app
  this.cssFiles = this.extractFiles('.css')
  this.jsFiles = this.extractFiles('.js')

  this.appTemplate = fs.readFileSync(path.resolve(__dirname, './fixtures/index.html.mustache'), 'utf-8')
}

/**
 * @param {string=} file
 * @returns {Object}
 */
RSG.prototype.readConfig = function (file) {

  file = file === undefined ? 'styleguide.json' : file

  if (!file) { return {} }

  var src = process.cwd() + '/' + file

  this.log.info({ configFile: src }, 'Reading config file')

  return fs.existsSync(src) ? fs.readJsonSync(src) : {}
}

/**
 * Generates the files to process for react-docgen
 */
RSG.prototype.getReactPropDocFiles = function () {
  var files = this.opts['reactDocgen'].files
  var fileList = []

  files.forEach(function (file) {
    fileList = fileList.concat(glob.sync(file, { realpath: true }))
  })

  // remove dupes
  return fileList.filter(function (elem, pos) {
    return fileList.indexOf(elem) === pos
  })

}

/**
 * Generates propType metadata using react-docgen
 * @returns {Promise}
 */
RSG.prototype.genReactPropDoc = function () {
  var self = this

  return new Promise(function (resolve, reject) {

    self.reactDocGenFiles.forEach(function (file) {
      try {

        var fileStat = fs.statSync(file)

        // If we do not have the file in our cache, or the mod time has changed, parse the file
        if (!self.reactDocGenCache[file] || self.reactDocGenCache[file] && self.reactDocGenCache[file] !== fileStat.mtime) {
          var srcData = fs.readFileSync(file)

          var propMetadata = reactDocGen.parse(srcData)

          var displayName = propMetadata.displayName

          if (displayName) {
            // only check for duplicate displayNames on the first pass
            // subsequent passes, we can assume we're regenerating the prop information
            if (self.reactPropMetas[displayName] && !self.reactDocGenInit) {
              reject(new Error('displayName already exists: ' + displayName))
            } else {
              self.reactPropMetas[displayName] = propMetadata
            }
          }

          self.reactDocGenCache[file] = fileStat.mtime
        }

      } catch (e) {
        // self.log.error(e, 'react-docgen error processing: %s', file)
      }

    })

    self.reactDocGenInit = true

    resolve()
  })
}

/**
 * @param {string} src
 * @param {string} dest
 * @returns {Promise}
 */
RSG.prototype.copy = function (src, dest) {
  return new Promise(function (resolve, reject) {
    fs.copy(src, dest, function (err) {
      if (err) { return reject(err) }
      resolve()
    })
  })
}

/**
 * @returns {Promise}
 */
RSG.prototype.copyAppFiles = function () {
  var src = path.resolve(__dirname, '../dist')
  var dest = this.opts.output + '/src'

  return this.copy(src, dest)
}

/**
 * @returns {Promise}
 */
RSG.prototype.copyOptsFiles = function () {
  var copy = this.copy
  var output = this.opts.output

  var files = this.opts.files.filter(function (file) {
    return fs.existsSync(file)
  })

  return Promise.all(
    files.map(function (file) {
      var dest = output + '/files/' + path.basename(file)
      return copy(file, dest)
    })
  )
}

/**
 * @param {string} ext
 * @returns {string[]}
 */
RSG.prototype.extractFiles = function (ext) {
  return this.opts.files
    .filter(function (file) { return path.extname(file) === ext })
    .map(function (file) {

      // linter had issues with a ternary operator
      // resulting in 'Infix operators must be spaced'
      // unsure how it passed on master
      if (fs.existsSync(file)) {
        return ('files/' + path.basename(file))
      }

      return file
    })
}

/**
 * @returns {Promise}
 */
RSG.prototype.createHtmlFile = function () {
  this.genReactPropDoc().then(function () {
    var data = assign({}, this.opts, {
      hashbang: this.opts.pushstate ? 'false' : 'true',
      reactVersion: this.reactVersion,
      cssFils: this.cssFiles,
      jsFils: this.jsFiles,
      propMetas: JSON.stringify(this.reactPropMetas),
      typekit: this.opts.typekit
    })

    var dest = this.opts.output + '/index.html'
    var rendered = mustache.render(this.appTemplate, data)

    return new Promise(function (resolve, reject) {
      fs.outputFile(dest, rendered, function (err) {
        if (err) { return reject(err) }
        resolve()
      })
    })
  }.bind(this))
}

/**
 * @returns {Promise}
 */
RSG.prototype.createReactFile = function () {
  var dest = this.opts.output + '/src/react_' + this.reactVersion + '.js'

  if (fs.existsSync(dest)) { return Promise.resolve() }

  var b = browserify({ debug: true })
    .require('react')
    .require('react/addons')

  return new Promise(function (resolve, reject) {
    b.bundle(function (err, buffer) {
      if (err) { return reject(err) }

      fs.outputFile(dest, buffer, function (outputFileErr) {
        if (outputFileErr) { return reject(outputFileErr) }
        resolve()
      })
    })
  })
}

/**
 * @returns {Promise}
 */
RSG.prototype.createContentsFile = function () {
  var self = this
  var contentsInter = this.opts.output + '/src/contents-inter.js'
  var dest = this.opts.output + '/src/contents.js'
  var data = this.input.map(function (file) {
      self.log.debug({ file: file }, 'styleguide input file')
      return "require('" + file + "')"
    }
  )

  data = 'module.exports = [' + data.join(',') + ']'
  fs.outputFileSync(contentsInter, data)

  var bundle = browserify(contentsInter, self.opts.browserifyConfig)

  bundle.transform(babelify.configure(self.opts.babelConfig))
    .external('react')
    .external('react/addons')

  function distContentsFile () {
    return new Promise(function (resolve, reject) {
      bundle.bundle(function (err, buffer) {
        if (err) { return reject(err) }

        fs.outputFile(dest, buffer, function (outputFileErr) {
          if (outputFileErr) { return reject(outputFileErr) }
          resolve()
        })

      })
    })
  }

  function watchedContentsFile () {
    self.log.info('styleguide watch mode enabled')
    bundle = watchify(bundle)

    return new Promise(function (resolve, reject) {
      bundle.bundle(function(err, buffer) {
        if (err) { return reject(err) }

        fs.outputFile(dest, buffer, function (outputFileErr) {
          if (outputFileErr) { return reject(outputFileErr) }
          resolve()
        })

      }).on('data', function() {});

      bundle.on('update', function () {
        self.log.info('styleguide updating')

        Promise.all([
          self.createHtmlFile(),
          distContentsFile()
        ]).then(function () {
        }).catch(function (err) {
          self.log.error(err, 'browserify update error')
        })
      })

      bundle.on('error', function (err) {
        self.log.error(err, 'browserify error')
      })

      bundle.on('time', function (time) {
        self.log.info('styleguide rebundled in ' + time + 'ms')
      })
    })
  }

  if (self.opts.watch) {
    return watchedContentsFile();
  } else {
    return distContentsFile()
  }

}

/**
 * @param {Function=} cb
 * @returns {Promise}
 */
RSG.prototype.generate = function (cb) {
  cb = cb ? cb.bind(this) : function () {}

  return Promise
    .all([
      this.copyAppFiles(),
      this.copyOptsFiles(),
      this.createHtmlFile(),
      this.createReactFile(),
      this.createContentsFile()
    ])
    .then(function () { cb() })
    .catch(function (err) {
      this.log.error(err)
      cb(err)
    }.bind(this))
}

module.exports = function (input, opts) {
  return new RSG(input, opts)
}
<commit_msg>Watch files and copy again when changed<commit_after>require('babel/register')

var path = require('path')
var fs = require('fs-extra')
var assign = require('object-assign')
var browserify = require('browserify')
var babelify = require('babelify')
var glob = require('glob')
var mustache = require('mustache')
var reactDocGen = require('react-docgen')
var watchify = require('watchify')

/**
 * React Styleguide Generator
 *
 * @class
 * @param {string} input
 * @param {Object} opts
 * @param {string=} opts.output
 * @param {string=} opts.title
 * @param {string=|Object} opts.config
 * @param {string=} opts.root
 * @param {boolean=} opts.pushstate
 * @param {string[]=} opts.files
 * @param {Object=} opts.babelConfig
 * @param {Object=} opts.browserifyConfig
 * @param {Object=} opts.reactDocgen
 * @param {Object=} opts.typekit
 */
function RSG (input, opts) {
  opts = opts || {}

  this.log = require('./logger')('rsg-lib', { debug: opts.verbose || false })

  var config

  // If feeding in a direct config object
  if (opts.config !== null && typeof opts.config === 'object') {
    config = opts.config
  } else {
    config = this.readConfig(opts.config)
  }

  opts = assign(config, opts)

  opts.output = path.resolve(process.cwd(), (opts.output || 'styleguide').replace(/\/+$/, ''))
  opts.title = opts.title || 'Style Guide'
  opts.root = opts.root ? path.normalize('/' + opts.root.replace(/\/+$/, '')) : null
  opts.pushstate = opts.pushstate || false
  opts.files = opts.files || []
  opts.babelConfig = opts.babelConfig || { stage: 0 }
  opts.browserifyConfig = assign({ debug: true }, opts.browserifyConfig, {
    cache: {},
    packageCache: {},
    standalone: 'Contents'
  })

  this.input = glob.sync(input, { realpath: true })

  opts['reactDocgen'] = opts['reactDocgen'] || {}

  if (!opts['reactDocgen'].files) {
    opts['reactDocgen'].files = [input]
  }

  this.opts = opts
  this.reactVersion = require('react').version

  // stores prop documentation generated from react-docgen
  this.reactPropMetas = {}

  // files to parse for react-docgen
  this.reactDocGenFiles = this.getReactPropDocFiles()

  // Cache of file modification times so we're not re-parsing
  // files that have not changed in react-docgen for perf
  this.reactDocGenCache = {}

  // has the first-pass of parsing happened?
  this.reactDocGenInit = false

  // Cached files to include into the styleguide app
  this.cssFiles = this.extractFiles('.css')
  this.jsFiles = this.extractFiles('.js')

  this.appTemplate = fs.readFileSync(path.resolve(__dirname, './fixtures/index.html.mustache'), 'utf-8')
}

RSG.prototype.readConfig = function (file) {
  file = file === undefined ? 'styleguide.json' : file

  if (!file) { return {} }

  var src = process.cwd() + '/' + file

  this.log.info({ configFile: src }, 'Reading config file')

  return fs.existsSync(src) ? fs.readJsonSync(src) : {}
}

RSG.prototype.getReactPropDocFiles = function () {
  var files = this.opts['reactDocgen'].files
  var fileList = []

  files.forEach(function (file) {
    fileList = fileList.concat(glob.sync(file, { realpath: true }))
  })

  // remove dupes
  return fileList.filter(function (elem, pos) {
    return fileList.indexOf(elem) === pos
  })
}

RSG.prototype.genReactPropDoc = function () {
  var self = this

  return new Promise(function (resolve, reject) {

    self.reactDocGenFiles.forEach(function (file) {
      try {

        var fileStat = fs.statSync(file)

        // If we do not have the file in our cache, or the mod time has changed, parse the file
        if (!self.reactDocGenCache[file] || self.reactDocGenCache[file] && self.reactDocGenCache[file] !== fileStat.mtime) {
          var srcData = fs.readFileSync(file)

          var propMetadata = reactDocGen.parse(srcData)

          var displayName = propMetadata.displayName

          if (displayName) {
            // only check for duplicate displayNames on the first pass
            // subsequent passes, we can assume we're regenerating the prop information
            if (self.reactPropMetas[displayName] && !self.reactDocGenInit) {
              reject(new Error('displayName already exists: ' + displayName))
            } else {
              self.reactPropMetas[displayName] = propMetadata
            }
          }

          self.reactDocGenCache[file] = fileStat.mtime
        }

      } catch (e) {
        // self.log.error(e, 'react-docgen error processing: %s', file)
      }

    })

    self.reactDocGenInit = true

    resolve()
  })
}

RSG.prototype.copy = function (src, dest) {
  return new Promise(function (resolve, reject) {
    fs.copy(src, dest, function (err) {
      if (err) { return reject(err) }
      resolve()
    })
  })
}

RSG.prototype.copyAppFiles = function () {
  var src = path.resolve(__dirname, '../dist')
  var dest = this.opts.output + '/src'

  return this.copy(src, dest)
}

RSG.prototype.copyOptsFiles = function () {
  var self = this;
  var copy = self.copy
  var output = self.opts.output

  var files = self.opts.files.filter(function (file) {
    return fs.existsSync(file)
  })

  function copyFile(file, log) {
    if (log)
      self.log.info({ file: file }, 'file changed');

    var dest = output + '/files/' + path.basename(file)
    return copy(file, dest)
  }

  if (self.opts.watch) {
    files.forEach(function(file) {
      fs.watchFile(file, copyFile.bind(this, file, true))
    });
  }

  return Promise.all(files.map(copyFile))
}

RSG.prototype.extractFiles = function (ext) {
  return this.opts.files
    .filter(function (file) { return path.extname(file) === ext })
    .map(function (file) {

      // linter had issues with a ternary operator
      // resulting in 'Infix operators must be spaced'
      // unsure how it passed on master
      if (fs.existsSync(file)) {
        return ('files/' + path.basename(file))
      }

      return file
    })
}

RSG.prototype.createHtmlFile = function () {
  this.genReactPropDoc().then(function () {
    var data = assign({}, this.opts, {
      hashbang: this.opts.pushstate ? 'false' : 'true',
      reactVersion: this.reactVersion,
      cssFils: this.cssFiles,
      jsFils: this.jsFiles,
      propMetas: JSON.stringify(this.reactPropMetas),
      typekit: this.opts.typekit
    })

    var dest = this.opts.output + '/index.html'
    var rendered = mustache.render(this.appTemplate, data)

    return new Promise(function (resolve, reject) {
      fs.outputFile(dest, rendered, function (err) {
        if (err) { return reject(err) }
        resolve()
      })
    })
  }.bind(this))
}

RSG.prototype.createReactFile = function () {
  var dest = this.opts.output + '/src/react_' + this.reactVersion + '.js'

  if (fs.existsSync(dest)) { return Promise.resolve() }

  var b = browserify({ debug: true })
    .require('react')
    .require('react/addons')

  return new Promise(function (resolve, reject) {
    b.bundle(function (err, buffer) {
      if (err) { return reject(err) }

      fs.outputFile(dest, buffer, function (outputFileErr) {
        if (outputFileErr) { return reject(outputFileErr) }
        resolve()
      })
    })
  })
}

RSG.prototype.createContentsFile = function () {
  var self = this
  var contentsInter = this.opts.output + '/src/contents-inter.js'
  var dest = this.opts.output + '/src/contents.js'
  var data = this.input.map(function (file) {
      self.log.debug({ file: file }, 'styleguide input file')
      return "require('" + file + "')"
    }
  )

  data = 'module.exports = [' + data.join(',') + ']'
  fs.outputFileSync(contentsInter, data)

  var bundle = browserify(contentsInter, self.opts.browserifyConfig)

  bundle.transform(babelify.configure(self.opts.babelConfig))
    .external('react')
    .external('react/addons')

  function distContentsFile () {
    return new Promise(function (resolve, reject) {
      bundle.bundle(function (err, buffer) {
        if (err) { return reject(err) }

        fs.outputFile(dest, buffer, function (outputFileErr) {
          if (outputFileErr) { return reject(outputFileErr) }
          resolve()
        })

      })
    })
  }

  function watchedContentsFile () {
    self.log.info('styleguide watch mode enabled')
    bundle = watchify(bundle)

    return new Promise(function (resolve, reject) {
      bundle.bundle(function(err, buffer) {
        if (err) { return reject(err) }

        fs.outputFile(dest, buffer, function (outputFileErr) {
          if (outputFileErr) { return reject(outputFileErr) }
          resolve()
        })

      }).on('data', function() {});

      bundle.on('update', function () {
        self.log.info('styleguide updating')

        Promise.all([
          self.createHtmlFile(),
          distContentsFile()
        ]).then(function () {
        }).catch(function (err) {
          self.log.error(err, 'browserify update error')
        })
      })

      bundle.on('error', function (err) {
        self.log.error(err, 'browserify error')
      })

      bundle.on('time', function (time) {
        self.log.info('styleguide rebundled in ' + time + 'ms')
      })
    })
  }

  if (self.opts.watch) {
    return watchedContentsFile();
  } else {
    return distContentsFile()
  }

}

RSG.prototype.generate = function (cb) {
  cb = cb ? cb.bind(this) : function () {}

  return Promise
    .all([
      this.copyAppFiles(),
      this.copyOptsFiles(),
      this.createHtmlFile(),
      this.createReactFile(),
      this.createContentsFile()
    ])
    .then(function () { cb() })
    .catch(function (err) {
      this.log.error(err)
      cb(err)
    }.bind(this))
}

module.exports = function (input, opts) {
  return new RSG(input, opts)
}
<|endoftext|>