<commit_before>const React = require('react')
const { findDOMNode } = require('react-dom')
const scrollIntoView = require('dom-scroll-into-view')

let _debugStates = []

let Autocomplete = React.createClass({

  propTypes: {
    /**
     * The items to display in the dropdown menu
     */
    items: React.PropTypes.array.isRequired,
    /**
     * The value to display in the input field
     */
    value: React.PropTypes.any,
    /**
     * Arguments: `event: Event, value: String`
     *
     * Invoked every time the user changes the input's value.
     */
    onChange: React.PropTypes.func,
    /**
     * Arguments: `value: String, item: Any`
     *
     * Invoked when the user selects an item from the dropdown menu.
     */
    onSelect: React.PropTypes.func,
    /**
     * Arguments: `item: Any, value: String`
     *
     * Invoked for each entry in `items` and its return value is used to
     * determine whether or not it should be displayed in the dropdown menu.
     * By default all items are always rendered.
     */
    shouldItemRender: React.PropTypes.func,
    /**
     * Arguments: `itemA: Any, itemB: Any, value: String`
     *
     * The function which is used to sort `items` before display.
     */
    sortItems: React.PropTypes.func,
    /**
     * Arguments: `item: Any`
     *
     * Used to read the display value from each entry in `items`.
     */
    getItemValue: React.PropTypes.func.isRequired,
    /**
     * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
     *
     * Invoked for each entry in `items` that also passes `shouldItemRender` to
     * generate the render tree for each item in the dropdown menu. `styles` is
     * an optional set of styles that can be applied to improve the look/feel
     * of the items in the dropdown menu.
     */
    renderItem: React.PropTypes.func.isRequired,
    /**
     * Arguments: `items: Array<Any>, value: String, styles: Object`
     *
     * Invoked to generate the render tree for the dropdown menu. Ensure the
     * returned tree includes `items` or else no items will be rendered.
     * `styles` will contain { top, left, minWidth } which are the coordinates
     * of the top-left corner and the width of the dropdown menu.
     */
    renderMenu: React.PropTypes.func,
    /**
     * Styles that are applied to the dropdown menu in the default `renderMenu`
     * implementation. If you override `renderMenu` and you want to use
     * `menuStyles` you must manually apply them (`this.props.menuStyles`).
     */
    menuStyle: React.PropTypes.object,
    /**
     * Props that are applied to the `<input />` element rendered by
     * `Autocomplete`. Any properties supported by `HTMLInputElement` can be
     * specified, apart from the following which are set by `Autocomplete`:
     * value, autoComplete, role, aria-autocomplete
     */
    inputProps: React.PropTypes.object,
    /**
     * Props that are applied to the element which wraps the `<input />` and
     * dropdown menu elements rendered by `Autocomplete`.
     */
    wrapperProps: React.PropTypes.object,
    /**
     * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
     * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
     * will win if it contains a `style` entry.
     */
    wrapperStyle: React.PropTypes.object,
    /**
     * Whether or not to automatically highlight the top match in the dropdown
     * menu.
     */
    autoHighlight: React.PropTypes.bool,
    /**
     * Arguments: `isOpen: Boolean`
     *
     * Invoked every time the dropdown menu's visibility changes (i.e. every
     * time it is displayed/hidden).
     */
    onMenuVisibilityChange: React.PropTypes.func,
    /**
     * Used to override the internal logic which displays/hides the dropdown
     * menu. This is useful if you want to force a certain state based on your
     * UX/business logic. Use it together with `onMenuVisibilityChange` for
     * fine-grained control over the dropdown menu dynamics.
     */
    open: React.PropTypes.bool,
    debug: React.PropTypes.bool,
  },

  getDefaultProps () {
    return {
      value: '',
      wrapperProps: {},
      wrapperStyle: {
        display: 'inline-block'
      },
      inputProps: {},
      onChange () {},
      onSelect () {},
      renderMenu (items, value, style) {
        return

      },
      menuStyle: {
        borderRadius: '3px',
        boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
        background: 'rgba(255, 255, 255, 0.9)',
        padding: '2px 0',
        fontSize: '90%',
        position: 'fixed',
        overflow: 'auto',
        maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom
      },
      autoHighlight: true,
      onMenuVisibilityChange () {},
    }
  },

  getInitialState () {
    return {
      isOpen: false,
      highlightedIndex: null,
    }
  },

  componentWillMount () {
    this._ignoreBlur = false
    this._performAutoCompleteOnUpdate = false
    this._performAutoCompleteOnKeyUp = false
  },

  componentWillReceiveProps (nextProps) {
    this._performAutoCompleteOnUpdate = true
    // If `items` has changed we want to reset `highlightedIndex`
    // since it probably no longer refers to a relevant item
    if (this.props.items !== nextProps.items ||
      // The entries in `items` may have been changed even though the
      // object reference remains the same, double check by seeing
      // if `highlightedIndex` points to an existing item
      this.state.highlightedIndex >= nextProps.items.length) {
      this.setState({ highlightedIndex: null })
    }
  },

  componentDidUpdate (prevProps, prevState) {
    if (this.state.isOpen === true && prevState.isOpen === false)
      this.setMenuPositions()

    if (this.state.isOpen && this._performAutoCompleteOnUpdate) {
      this._performAutoCompleteOnUpdate = false
      this.maybeAutoCompleteText()
    }

    this.maybeScrollItemIntoView()
    if (prevState.isOpen !== this.state.isOpen) {
      this.props.onMenuVisibilityChange(this.state.isOpen)
    }
  },

  maybeScrollItemIntoView () {
    if (this.state.isOpen === true && this.state.highlightedIndex !== null) {
      var itemNode = this.refs[`item-${this.state.highlightedIndex}`]
      var menuNode = this.refs.menu
      scrollIntoView(
        findDOMNode(itemNode),
        findDOMNode(menuNode),
        { onlyScrollIfNeeded: true }
      )
    }
  },

  handleKeyDown (event) {
    if (this.keyDownHandlers[event.key])
      this.keyDownHandlers[event.key].call(this, event)
    else {
      this.setState({
        highlightedIndex: null,
        isOpen: true
      })
    }
  },

  handleChange (event) {
    this._performAutoCompleteOnKeyUp = true
    this.props.onChange(event, event.target.value)
  },

  handleKeyUp () {
    if (this._performAutoCompleteOnKeyUp) {
      this._performAutoCompleteOnKeyUp = false
      this.maybeAutoCompleteText()
    }
  },

  keyDownHandlers: {
    ArrowDown (event) {
      event.preventDefault()
      const itemsLength = this.getFilteredItems().length
      if (!itemsLength) return
      var { highlightedIndex } = this.state
      var index = (
        highlightedIndex === null ||
        highlightedIndex === itemsLength - 1
      ) ?  0 : highlightedIndex + 1
      this._performAutoCompleteOnKeyUp = true
      this.setState({
        highlightedIndex: index,
        isOpen: true,
      })
    },

    ArrowUp (event) {
      event.preventDefault()
      const itemsLength = this.getFilteredItems().length
      if (!itemsLength) return
      var { highlightedIndex } = this.state
      var index = (
        highlightedIndex === 0 ||
        highlightedIndex === null
      ) ? itemsLength - 1 : highlightedIndex - 1
      this._performAutoCompleteOnKeyUp = true
      this.setState({
        highlightedIndex: index,
        isOpen: true,
      })
    },

    Enter (event) {
      if (this.state.isOpen === false) {
        // menu is closed so there is no selection to accept -> do nothing
        return
      }
      else if (this.state.highlightedIndex == null) {
        // input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input
        this.setState({
          isOpen: false
        }, () => {
          this.refs.input.select()
        })
      }
      else {
        // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
        event.preventDefault()
        var item = this.getFilteredItems()[this.state.highlightedIndex]
        var value = this.props.getItemValue(item)
        this.setState({
          isOpen: false,
          highlightedIndex: null
        }, () => {
          //this.refs.input.focus() // TODO: file issue
          this.refs.input.setSelectionRange(
            value.length,
            value.length
          )
          this.props.onSelect(value, item)
        })
      }
    },

    Escape (event) {
      this.setState({
        highlightedIndex: null,
        isOpen: false
      })
    }
  },

  getFilteredItems () {
    let items = this.props.items

    if (this.props.shouldItemRender) {
      items = items.filter((item) => (
        this.props.shouldItemRender(item, this.props.value)
      ))
    }

    if (this.props.sortItems) {
      items.sort((a, b) => (
        this.props.sortItems(a, b, this.props.value)
      ))
    }

    return items
  },

  maybeAutoCompleteText () {
    if (!this.props.autoHighlight || this.props.value === '')
      return
    var { highlightedIndex } = this.state
    var items = this.getFilteredItems()
    if (items.length === 0)
      return
    var matchedItem = highlightedIndex !== null ?
      items[highlightedIndex] : items[0]
    var itemValue = this.props.getItemValue(matchedItem)
    var itemValueDoesMatch = (itemValue.toLowerCase().indexOf(
      this.props.value.toLowerCase()
    ) === 0)
    if (itemValueDoesMatch && highlightedIndex === null)
      this.setState({ highlightedIndex: 0 })
  },

  setMenuPositions () {
    var node = this.refs.input
    var rect = node.getBoundingClientRect()
    var computedStyle = global.window.getComputedStyle(node)
    var marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
    var marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
    var marginRight = parseInt(computedStyle.marginRight, 10) || 0;
    this.setState({
      menuTop: rect.bottom + marginBottom,
      menuLeft: rect.left + marginLeft,
      menuWidth: rect.width + marginLeft + marginRight
    })
  },

  highlightItemFromMouse (index) {
    this.setState({ highlightedIndex: index })
  },

  selectItemFromMouse (item) {
    var value = this.props.getItemValue(item);
    this.setState({
      isOpen: false,
      highlightedIndex: null
    }, () => {
      this.props.onSelect(value, item)
      this.refs.input.focus()
    })
  },

  setIgnoreBlur (ignore) {
    this._ignoreBlur = ignore
  },

  renderMenu () {
    var items = this.getFilteredItems().map((item, index) => {
      var element = this.props.renderItem(
        item,
        this.state.highlightedIndex === index,
        {cursor: 'default'}
      )
      return React.cloneElement(element, {
        onMouseDown: () => this.setIgnoreBlur(true), // Ignore blur to prevent menu from de-rendering before we can process click
        onMouseEnter: () => this.highlightItemFromMouse(index),
        onClick: () => this.selectItemFromMouse(item),
        ref: `item-${index}`,
      })
    })
    var style = {
      left: this.state.menuLeft,
      top: this.state.menuTop,
      minWidth: this.state.menuWidth,
    }
    var menu = this.props.renderMenu(items, this.props.value, style)
    return React.cloneElement(menu, { ref: 'menu' })
  },

  handleInputBlur () {
    if (this._ignoreBlur)
      return
    this.setState({
      isOpen: false,
      highlightedIndex: null
    })
  },

  handleInputFocus () {
    if (this._ignoreBlur) {
      this.setIgnoreBlur(false)
      return
    }
    // We don't want `selectItemFromMouse` to trigger when
    // the user clicks into the input to focus it, so set this
    // flag to cancel out the logic in `handleInputClick`.
    // The event order is:  MouseDown -> Focus -> MouseUp -> Click
    this._ignoreClick = true
    this.setState({ isOpen: true })
  },

  isInputFocused () {
    var el = this.refs.input
    return el.ownerDocument && (el === el.ownerDocument.activeElement)
  },

  handleInputClick () {
    // Input will not be focused if it's disabled
    if (this.isInputFocused() && this.state.isOpen === false)
      this.setState({ isOpen: true })
    else if (this.state.highlightedIndex !== null && !this._ignoreClick)
      this.selectItemFromMouse(this.getFilteredItems()[this.state.highlightedIndex])
    this._ignoreClick = false
  },

  composeEventHandlers (internal, external) {
    return external
      ? e => { internal(e); external(e); }
      : internal
  },

  render () {
    if (this.props.debug) { // you don't like it, you love it
      _debugStates.push({
        id: _debugStates.length,
        state: this.state
      })
    }

    const { inputProps } = this.props
    return (

        <input
          {...inputProps}
          role="combobox"
          aria-autocomplete="list"
          autoComplete="off"
          ref="input"
          onFocus={this.composeEventHandlers(this.handleInputFocus, inputProps.onFocus)}
          onBlur={this.composeEventHandlers(this.handleInputBlur, inputProps.onBlur)}
          onChange={this.handleChange}
          onKeyDown={this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown)}
          onKeyUp={this.composeEventHandlers(this.handleKeyUp, inputProps.onKeyUp)}
          onClick={this.composeEventHandlers(this.handleInputClick, inputProps.onClick)}
          value={this.props.value}
        />
        {('open' in this.props ? this.props.open : this.state.isOpen) && this.renderMenu()}
        {this.props.debug && (

            {JSON.stringify(_debugStates.slice(_debugStates.length - 5, _debugStates.length), null, 2)}

        )}

    )
  }
})

module.exports = Autocomplete

<commit_msg>Create local reference to React.PropTypes<commit_after>const React = require('react')
const { PropTypes } = React
const { findDOMNode } = require('react-dom')
const scrollIntoView = require('dom-scroll-into-view')

let _debugStates = []

let Autocomplete = React.createClass({

  propTypes: {
    /**
     * The items to display in the dropdown menu
     */
    items: PropTypes.array.isRequired,
    /**
     * The value to display in the input field
     */
    value: PropTypes.any,
    /**
     * Arguments: `event: Event, value: String`
     *
     * Invoked every time the user changes the input's value.
     */
    onChange: PropTypes.func,
    /**
     * Arguments: `value: String, item: Any`
     *
     * Invoked when the user selects an item from the dropdown menu.
     */
    onSelect: PropTypes.func,
    /**
     * Arguments: `item: Any, value: String`
     *
     * Invoked for each entry in `items` and its return value is used to
     * determine whether or not it should be displayed in the dropdown menu.
     * By default all items are always rendered.
     */
    shouldItemRender: PropTypes.func,
    /**
     * Arguments: `itemA: Any, itemB: Any, value: String`
     *
     * The function which is used to sort `items` before display.
     */
    sortItems: PropTypes.func,
    /**
     * Arguments: `item: Any`
     *
     * Used to read the display value from each entry in `items`.
     */
    getItemValue: PropTypes.func.isRequired,
    /**
     * Arguments: `item: Any, isHighlighted: Boolean, styles: Object`
     *
     * Invoked for each entry in `items` that also passes `shouldItemRender` to
     * generate the render tree for each item in the dropdown menu. `styles` is
     * an optional set of styles that can be applied to improve the look/feel
     * of the items in the dropdown menu.
     */
    renderItem: PropTypes.func.isRequired,
    /**
     * Arguments: `items: Array<Any>, value: String, styles: Object`
     *
     * Invoked to generate the render tree for the dropdown menu. Ensure the
     * returned tree includes `items` or else no items will be rendered.
     * `styles` will contain { top, left, minWidth } which are the coordinates
     * of the top-left corner and the width of the dropdown menu.
     */
    renderMenu: PropTypes.func,
    /**
     * Styles that are applied to the dropdown menu in the default `renderMenu`
     * implementation. If you override `renderMenu` and you want to use
     * `menuStyles` you must manually apply them (`this.props.menuStyles`).
     */
    menuStyle: PropTypes.object,
    /**
     * Props that are applied to the `<input />` element rendered by
     * `Autocomplete`. Any properties supported by `HTMLInputElement` can be
     * specified, apart from the following which are set by `Autocomplete`:
     * value, autoComplete, role, aria-autocomplete
     */
    inputProps: PropTypes.object,
    /**
     * Props that are applied to the element which wraps the `<input />` and
     * dropdown menu elements rendered by `Autocomplete`.
     */
    wrapperProps: PropTypes.object,
    /**
     * This is a shorthand for `wrapperProps={{ style: <your styles> }}`.
     * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter
     * will win if it contains a `style` entry.
     */
    wrapperStyle: PropTypes.object,
    /**
     * Whether or not to automatically highlight the top match in the dropdown
     * menu.
     */
    autoHighlight: PropTypes.bool,
    /**
     * Arguments: `isOpen: Boolean`
     *
     * Invoked every time the dropdown menu's visibility changes (i.e. every
     * time it is displayed/hidden).
     */
    onMenuVisibilityChange: PropTypes.func,
    /**
     * Used to override the internal logic which displays/hides the dropdown
     * menu. This is useful if you want to force a certain state based on your
     * UX/business logic. Use it together with `onMenuVisibilityChange` for
     * fine-grained control over the dropdown menu dynamics.
     */
    open: PropTypes.bool,
    debug: PropTypes.bool,
  },

  getDefaultProps () {
    return {
      value: '',
      wrapperProps: {},
      wrapperStyle: {
        display: 'inline-block'
      },
      inputProps: {},
      onChange () {},
      onSelect () {},
      renderMenu (items, value, style) {
        return

      },
      menuStyle: {
        borderRadius: '3px',
        boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
        background: 'rgba(255, 255, 255, 0.9)',
        padding: '2px 0',
        fontSize: '90%',
        position: 'fixed',
        overflow: 'auto',
        maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom
      },
      autoHighlight: true,
      onMenuVisibilityChange () {},
    }
  },

  getInitialState () {
    return {
      isOpen: false,
      highlightedIndex: null,
    }
  },

  componentWillMount () {
    this._ignoreBlur = false
    this._performAutoCompleteOnUpdate = false
    this._performAutoCompleteOnKeyUp = false
  },

  componentWillReceiveProps (nextProps) {
    this._performAutoCompleteOnUpdate = true
    // If `items` has changed we want to reset `highlightedIndex`
    // since it probably no longer refers to a relevant item
    if (this.props.items !== nextProps.items ||
      // The entries in `items` may have been changed even though the
      // object reference remains the same, double check by seeing
      // if `highlightedIndex` points to an existing item
      this.state.highlightedIndex >= nextProps.items.length) {
      this.setState({ highlightedIndex: null })
    }
  },

  componentDidUpdate (prevProps, prevState) {
    if (this.state.isOpen === true && prevState.isOpen === false)
      this.setMenuPositions()

    if (this.state.isOpen && this._performAutoCompleteOnUpdate) {
      this._performAutoCompleteOnUpdate = false
      this.maybeAutoCompleteText()
    }

    this.maybeScrollItemIntoView()
    if (prevState.isOpen !== this.state.isOpen) {
      this.props.onMenuVisibilityChange(this.state.isOpen)
    }
  },

  maybeScrollItemIntoView () {
    if (this.state.isOpen === true && this.state.highlightedIndex !== null) {
      var itemNode = this.refs[`item-${this.state.highlightedIndex}`]
      var menuNode = this.refs.menu
      scrollIntoView(
        findDOMNode(itemNode),
        findDOMNode(menuNode),
        { onlyScrollIfNeeded: true }
      )
    }
  },

  handleKeyDown (event) {
    if (this.keyDownHandlers[event.key])
      this.keyDownHandlers[event.key].call(this, event)
    else {
      this.setState({
        highlightedIndex: null,
        isOpen: true
      })
    }
  },

  handleChange (event) {
    this._performAutoCompleteOnKeyUp = true
    this.props.onChange(event, event.target.value)
  },

  handleKeyUp () {
    if (this._performAutoCompleteOnKeyUp) {
      this._performAutoCompleteOnKeyUp = false
      this.maybeAutoCompleteText()
    }
  },

  keyDownHandlers: {
    ArrowDown (event) {
      event.preventDefault()
      const itemsLength = this.getFilteredItems().length
      if (!itemsLength) return
      var { highlightedIndex } = this.state
      var index = (
        highlightedIndex === null ||
        highlightedIndex === itemsLength - 1
      ) ?  0 : highlightedIndex + 1
      this._performAutoCompleteOnKeyUp = true
      this.setState({
        highlightedIndex: index,
        isOpen: true,
      })
    },

    ArrowUp (event) {
      event.preventDefault()
      const itemsLength = this.getFilteredItems().length
      if (!itemsLength) return
      var { highlightedIndex } = this.state
      var index = (
        highlightedIndex === 0 ||
        highlightedIndex === null
      ) ? itemsLength - 1 : highlightedIndex - 1
      this._performAutoCompleteOnKeyUp = true
      this.setState({
        highlightedIndex: index,
        isOpen: true,
      })
    },

    Enter (event) {
      if (this.state.isOpen === false) {
        // menu is closed so there is no selection to accept -> do nothing
        return
      }
      else if (this.state.highlightedIndex == null) {
        // input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input
        this.setState({
          isOpen: false
        }, () => {
          this.refs.input.select()
        })
      }
      else {
        // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
        event.preventDefault()
        var item = this.getFilteredItems()[this.state.highlightedIndex]
        var value = this.props.getItemValue(item)
        this.setState({
          isOpen: false,
          highlightedIndex: null
        }, () => {
          //this.refs.input.focus() // TODO: file issue
          this.refs.input.setSelectionRange(
            value.length,
            value.length
          )
          this.props.onSelect(value, item)
        })
      }
    },

    Escape (event) {
      this.setState({
        highlightedIndex: null,
        isOpen: false
      })
    }
  },

  getFilteredItems () {
    let items = this.props.items

    if (this.props.shouldItemRender) {
      items = items.filter((item) => (
        this.props.shouldItemRender(item, this.props.value)
      ))
    }

    if (this.props.sortItems) {
      items.sort((a, b) => (
        this.props.sortItems(a, b, this.props.value)
      ))
    }

    return items
  },

  maybeAutoCompleteText () {
    if (!this.props.autoHighlight || this.props.value === '')
      return
    var { highlightedIndex } = this.state
    var items = this.getFilteredItems()
    if (items.length === 0)
      return
    var matchedItem = highlightedIndex !== null ?
      items[highlightedIndex] : items[0]
    var itemValue = this.props.getItemValue(matchedItem)
    var itemValueDoesMatch = (itemValue.toLowerCase().indexOf(
      this.props.value.toLowerCase()
    ) === 0)
    if (itemValueDoesMatch && highlightedIndex === null)
      this.setState({ highlightedIndex: 0 })
  },

  setMenuPositions () {
    var node = this.refs.input
    var rect = node.getBoundingClientRect()
    var computedStyle = global.window.getComputedStyle(node)
    var marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
    var marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
    var marginRight = parseInt(computedStyle.marginRight, 10) || 0;
    this.setState({
      menuTop: rect.bottom + marginBottom,
      menuLeft: rect.left + marginLeft,
      menuWidth: rect.width + marginLeft + marginRight
    })
  },

  highlightItemFromMouse (index) {
    this.setState({ highlightedIndex: index })
  },

  selectItemFromMouse (item) {
    var value = this.props.getItemValue(item);
    this.setState({
      isOpen: false,
      highlightedIndex: null
    }, () => {
      this.props.onSelect(value, item)
      this.refs.input.focus()
    })
  },

  setIgnoreBlur (ignore) {
    this._ignoreBlur = ignore
  },

  renderMenu () {
    var items = this.getFilteredItems().map((item, index) => {
      var element = this.props.renderItem(
        item,
        this.state.highlightedIndex === index,
        {cursor: 'default'}
      )
      return React.cloneElement(element, {
        onMouseDown: () => this.setIgnoreBlur(true), // Ignore blur to prevent menu from de-rendering before we can process click
        onMouseEnter: () => this.highlightItemFromMouse(index),
        onClick: () => this.selectItemFromMouse(item),
        ref: `item-${index}`,
      })
    })
    var style = {
      left: this.state.menuLeft,
      top: this.state.menuTop,
      minWidth: this.state.menuWidth,
    }
    var menu = this.props.renderMenu(items, this.props.value, style)
    return React.cloneElement(menu, { ref: 'menu' })
  },

  handleInputBlur () {
    if (this._ignoreBlur)
      return
    this.setState({
      isOpen: false,
      highlightedIndex: null
    })
  },

  handleInputFocus () {
    if (this._ignoreBlur) {
      this.setIgnoreBlur(false)
      return
    }
    // We don't want `selectItemFromMouse` to trigger when
    // the user clicks into the input to focus it, so set this
    // flag to cancel out the logic in `handleInputClick`.
    // The event order is:  MouseDown -> Focus -> MouseUp -> Click
    this._ignoreClick = true
    this.setState({ isOpen: true })
  },

  isInputFocused () {
    var el = this.refs.input
    return el.ownerDocument && (el === el.ownerDocument.activeElement)
  },

  handleInputClick () {
    // Input will not be focused if it's disabled
    if (this.isInputFocused() && this.state.isOpen === false)
      this.setState({ isOpen: true })
    else if (this.state.highlightedIndex !== null && !this._ignoreClick)
      this.selectItemFromMouse(this.getFilteredItems()[this.state.highlightedIndex])
    this._ignoreClick = false
  },

  composeEventHandlers (internal, external) {
    return external
      ? e => { internal(e); external(e); }
      : internal
  },

  render () {
    if (this.props.debug) { // you don't like it, you love it
      _debugStates.push({
        id: _debugStates.length,
        state: this.state
      })
    }

    const { inputProps } = this.props
    return (

        <input
          {...inputProps}
          role="combobox"
          aria-autocomplete="list"
          autoComplete="off"
          ref="input"
          onFocus={this.composeEventHandlers(this.handleInputFocus, inputProps.onFocus)}
          onBlur={this.composeEventHandlers(this.handleInputBlur, inputProps.onBlur)}
          onChange={this.handleChange}
          onKeyDown={this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown)}
          onKeyUp={this.composeEventHandlers(this.handleKeyUp, inputProps.onKeyUp)}
          onClick={this.composeEventHandlers(this.handleInputClick, inputProps.onClick)}
          value={this.props.value}
        />
        {('open' in this.props ? this.props.open : this.state.isOpen) && this.renderMenu()}
        {this.props.debug && (

            {JSON.stringify(_debugStates.slice(_debugStates.length - 5, _debugStates.length), null, 2)}

        )}

    )
  }
})

module.exports = Autocomplete

<|endoftext|>


<commit_before>'use strict';

var utils = require('../utils');

module.exports = function(collection, options) {
	options = options || {};
	var format = options.format || 'timestamp';
	var dateFormatter = utils.createDateFormatter(format);

	var beforeInsert = function(params, callback) {
		var objs = params.obj || params.objs;
		if (!Array.isArray(objs)) objs = [objs];

		var createDate = dateFormatter(new Date());
		objs.forEach(function(obj) {
			if (obj.createDate === undefined) {
				obj.createDate = createDate;
			}
		});

		callback();
	};

	var beforeUpsert = function(params, callback) {
		var modifier = params.modifier;

		var isCreateDateProvided =
			(modifier.$setOnInsert && modifier.$setOnInsert.createDate !== undefined) ||
			(modifier.$set && modifier.$set.createDate !== undefined) ||
			modifier.createDate !== undefined;

		if (isCreateDateProvided) {
			return callback();
		}

		var setOnInsertModifier = {createDate: dateFormatter(new Date())};

		var isModifierEmpty = !Object.keys(modifier).length;
		if (isModifierEmpty) {
			params.modifier = {
				$setOnInsert: setOnInsertModifier
			};
		} else if (utils.isModifier(modifier)) {
			modifier.$setOnInsert = Object.asssign(
				modifier.$setOnInsert,
				setOnInsertModifier
			);
		} else {
			Object.assign(
				params.modifier,
				setOnInsertModifier
			);
		}

		callback();
	};

	collection.on('beforeInsertOne', beforeInsert);
	collection.on('beforeInsertMany', beforeInsert);
	collection.on('beforeUpsertOne', beforeUpsert);
};
<commit_msg>fix Object.assign usage<commit_after>'use strict';

var utils = require('../utils');

module.exports = function(collection, options) {
	options = options || {};
	var format = options.format || 'timestamp';
	var dateFormatter = utils.createDateFormatter(format);

	var beforeInsert = function(params, callback) {
		var objs = params.obj || params.objs;
		if (!Array.isArray(objs)) objs = [objs];

		var createDate = dateFormatter(new Date());
		objs.forEach(function(obj) {
			if (obj.createDate === undefined) {
				obj.createDate = createDate;
			}
		});

		callback();
	};

	var beforeUpsert = function(params, callback) {
		var modifier = params.modifier;

		var isCreateDateProvided =
			(modifier.$setOnInsert && modifier.$setOnInsert.createDate !== undefined) ||
			(modifier.$set && modifier.$set.createDate !== undefined) ||
			modifier.createDate !== undefined;

		if (isCreateDateProvided) {
			return callback();
		}

		var setOnInsertModifier = {createDate: dateFormatter(new Date())};

		var isModifierEmpty = !Object.keys(modifier).length;
		if (isModifierEmpty) {
			params.modifier = {
				$setOnInsert: setOnInsertModifier
			};
		} else if (utils.isModifier(modifier)) {
			modifier.$setOnInsert = Object.assign({},
				modifier.$setOnInsert,
				setOnInsertModifier
			);
		} else {
			Object.assign(
				params.modifier,
				setOnInsertModifier
			);
		}

		callback();
	};

	collection.on('beforeInsertOne', beforeInsert);
	collection.on('beforeInsertMany', beforeInsert);
	collection.on('beforeUpsertOne', beforeUpsert);
};
<|endoftext|>


<commit_before>"use strict";

/**
 * Small lib to go through the OneDrive authentication process and get back the token.
 *
 * Instructions:
 * - Host a copy of callback.html and odauth.js on your domain.
 * - Embed odauth.js in your app like this:
 *   <script src="odauth.js"></script>
 * - Create instance of OneDriveAuth and call auth() method to begin and whenever you
 *   need to make an API call. In first `callback` param you should provide handler of
 *   successful authorization. If you're making an api call in response to a user's click
 *   action, call auth(callback, true), otherwise just call auth(callback). The difference
 *   is that sometimes OneDriveAuth needs to pop up a window so the user can sign in,
 *   grant your app permission, etc. The pop up can only be launched in response
 *   to a user click, otherwise the browser's popup blocker will block it. When
 *   OneDriveAuth isn't called in click mode, it'll put a sign-in button at the top of
 *   your page for the user to click. When it's done, it'll remove that button.
 */
class OneDriveAuth {
  /**
   * @constructor
   * @param {Array} appInfo
   * @param {string} appInfo.clientId from App Settings
   * @param {string} appInfo.scopes separated by space ("onedrive.readonly wl.signin" for example)
   * @param {string} appInfo.redirectUri for callback popup
   * @param {string} [appInfo.redirectOrigin] origin of callback window
   * @param {boolean} [appInfo.requireHttps=true] check for https and throw an error if it's omitted
   */
  constructor(appInfo) {
    this.appInfo = Object.assign({}, appInfo); // clone parameters

    if (!appInfo.clientId) {
      throw "appInfo object should have `clientId` property set to your application id";
    }

    if (!appInfo.scopes) {
      throw "appInfo object should have `scopes` property set to the scopes your app needs";
    }

    if (!appInfo.redirectUri) {
      throw "appInfo object should have `redirectUri` property set to your redirect landing url";
    }

    if (!appInfo.redirectOrigin) {
      // get the scheme://host:port from redirectUri
      this.appInfo.redirectOrigin = appInfo.redirectUri.match(/^[\w:]+\/\/[^\/]+/)[0];
    }

    if (typeof appInfo.requireHttps === 'undefined') {
      this.appInfo.requireHttps = true;
    }

    var sep = this.appInfo.redirectUri.indexOf('?') < 0 ? '?' : '&';
    // adds ?clientId=... to the end of string or before the hash in the redirectUri
    // because the auth window doesn't get back any info about client from OneDrive
    this.appInfo.redirectUri = this.appInfo.redirectUri.replace(/(#|$)/, sep + 'clientId=' + this.appInfo.clientId + '$1');
    // list of handlers for successful authorization
    this.callbacks = [];

    // listen for callback's message with auth token
    window.addEventListener('message', this.onAuthenticated.bind(this), false);
  }

  /**
   * Callback to handle successful authorization
   * @callback onAuth
   * @param {string} token
   */

  /**
   * The main auth method. First, we check if we have the user's auth token stored
   * in a cookie. if so, we read it and immediately call your `callback` method.
   * If we can't find the auth cookie, we need to pop up a window and send the user
   * to Microsoft Account so that they can sign in or grant your app the permissions
   * it needs. Depending on whether or not auth() was called in response to a user
   * click, it will either pop up the auth window or display a sign-in button for
   * the user to click (which results in the pop-up). when the user finishes the
   * auth flow, the popup window redirects back to your hosted callback.html file,
   * which calls the onAuthCallback() static method below. It then sets the auth cookie
   * and calls given `callback` function after closing the auth window.
   *
   * Subsequent calls to auth() will usually complete immediately without the
   * popup because the cookie is still fresh.
   *
   * @param {onAuth} callback to call in case of success authorization
   * @param {boolean} [wasClicked=false] whether the call is result of a click or not
   * @return {boolean} if the app is authorized yet; `false` means authorization started
   * @throws if https is required but omitted
   */
  auth(callback, wasClicked) {
    this.ensureHttps();
    var token = this.getTokenFromCookie();
    // if `callback` was missed for whatever reason we transfer its value to `wasClicked`
    wasClicked = wasClicked || (callback === true);
    callback = (typeof callback === 'function') ? callback : null;

    if (token) {
      callback && callback(token);
      return true;
    }

    // would be called in OneDriveAuth.onAuthenticated() method
    callback && this.callbacks.push(callback);

    if (this.inProgress) return false;
    this.inProgress = true;

    if (wasClicked) {
      this.challengeForAuth();
    } else {
      this.showLoginButton();
    }
    return false;
  }

  /**
   * Check if current page loaded via HTTPS
   * @return {boolean}
   */
  static isHttps() {
    return window.location.protocol.toLowerCase() === "https:";
  }

  /**
   * For added security we check for https
   * @throws if https is required but omitted
   */
  ensureHttps() {
    if (this.appInfo.requireHttps && !this.isHttps()) {
      throw new Error("HTTPS is required to authorize this application for OneDrive");
    }
  }

  getTokenFromCookie() {
    var cookies = document.cookie;
    var name = "odauth=";
    var start = cookies.indexOf(name);
    if (start >= 0) {
      start += name.length;
      var end = cookies.indexOf(';', start);
      if (end < 0) {
        end = cookies.length;
      }

      var value = cookies.substring(start, end);
      return value;
    }

    return "";
  }

  /**
   * Called when a login button needs to be displayed for the user to click on.
   * It will insert a textual login link at the top of the page.
   */
  showLoginButton() {
    var callback = this.challengeForAuth.bind(this);
    var loginText = document.createElement('a');
    loginText.href = "#";
    loginText.id = "loginText";
    loginText.onclick = callback;
    loginText.innerText = "[sign in]";
    document.body.insertBefore(loginText, document.body.children[0]);
  }

  /**
   * Called with the login button created by showLoginButton() needs to be
   * removed. If a customLoginButton() function is defined by your app, it will
   * be called with 'false' passed in to indicate the button should be removed.
   * Otherwise it will remove the textual link that showLoginButton() created.
   */
  removeLoginButton() {
    var loginText = document.getElementById("loginText");
    if (loginText) {
      document.body.removeChild(loginText);
    }
  }

  /**
   * Send user to the Microsoft Account OAuth2.0 page to authorize the app
   * and return the user to your redirectUri.
   */
  challengeForAuth() {
    var appInfo = this.appInfo;
    var url =
      "https://login.live.com/oauth20_authorize.srf" +
      "?client_id=" + appInfo.clientId +
      "&scope=" + encodeURIComponent(appInfo.scopes) +
      "&response_type=token" +
      "&redirect_uri=" + encodeURIComponent(appInfo.redirectUri);
    this.popup(url);
  }

  popup(url) {
    var width = 525,
      height = 525,
      screenX = window.screenX,
      screenY = window.screenY,
      outerWidth = window.outerWidth,
      outerHeight = window.outerHeight;

    var left = screenX + Math.max(outerWidth - width, 0) / 2;
    var top = screenY + Math.max(outerHeight - height, 0) / 2;

    var features = [
      "width=" + width,
      "height=" + height,
      "top=" + top,
      "left=" + left,
      "status=no",
      "resizable=yes",
      "toolbar=no",
      "menubar=no",
      "scrollbars=yes"
    ];
    var popup = window.open(url, "oauth", features.join(","));
    if (!popup) {
      console.error("failed to pop up auth window");
    }

    popup.focus();
  }

  /**
   * Called from auth window in response to successful authorization
   * @param {Event} event object with sent data
   * @param {string} event.data.token
   * @param {string} event.data.clientId
   * @param {string} event.origin origin of callback popup window
   */
  onAuthenticated(event) {
    var callback, data = event.data, token = data.token;
    // skip the message addressed to another client or came from unknown location
    if (this.appInfo.clientId !== data.clientId) return false;
    if (this.appInfo.redirectOrigin !== event.origin) return false;

    this.inProgress = false;

    while (callback = this.callbacks.shift()) {
      callback(token);
    }
  }

  /**
   * Called from the callback page after OAuth authorization.
   * On success it save the token to cookie and call onAuthenticated() method
   * of corresponding OneDriveAuth instance in parent window.
   */
  static onAuthCallback() {
    var authInfo = this.getAuthInfoFromUrl();
    var token = authInfo["access_token"];
    var expiry = parseInt(authInfo["expires_in"]);
    var clientId = authInfo["clientId"];
    var origin = location.origin;

    this.setCookie(token, expiry);
    window.opener.postMessage({ token, clientId }, origin);
    window.close();
  }

  static getAuthInfoFromUrl() {
    if (window.location.hash) {
      var authResponse = (window.location.search + window.location.hash).substr(1);
      var authInfo = JSON.parse(
        '{"' + authResponse.replace(/[&#]/g, '","').replace(/=/g, '":"') + '"}',
        (key, value) => key === "" ? value : decodeURIComponent(value)
      );
      return authInfo;
    } else {
      console.error("failed to receive auth token");
    }
  }

  static setCookie(token, expiresInSeconds) {
    var expiration = new Date();
    expiration.setTime(expiration.getTime() + expiresInSeconds * 1000);
    var cookie = "odauth=" + token + "; path=/; expires=" + expiration.toUTCString();

    if (this.isHttps()) {
      cookie = cookie + ";secure";
    }

    document.cookie = cookie;
  }
}

export default OneDriveAuth;
<commit_msg>Don't deal with buttons anymore!<commit_after>"use strict";

/**
 * Small lib to go through the OneDrive authentication process and get back the token.
 *
 * Instructions:
 * - Host a copy of callback.html and odauth.js on your domain.
 * - Embed odauth.js in your app like this:
 *   <script src="odauth.js"></script>
 * - Create instance of OneDriveAuth and call auth() method to begin and whenever you
 *   need to make an API call. In first `callback` param you should provide handler of
 *   successful authorization. If you're making an api call in response to a user's click
 *   action, call auth(callback, true), otherwise just call auth(callback). The difference
 *   is that sometimes OneDriveAuth needs to pop up a window so the user can sign in,
 *   grant your app permission, etc. The pop up can only be launched in response
 *   to a user click, otherwise the browser's popup blocker will block it. When
 *   OneDriveAuth isn't called in click mode, it'll put a sign-in button at the top of
 *   your page for the user to click. When it's done, it'll remove that button.
 */
class OneDriveAuth {
  /**
   * @constructor
   * @param {Array} appInfo
   * @param {string} appInfo.clientId from App Settings
   * @param {string} appInfo.scopes separated by space ("onedrive.readonly wl.signin" for example)
   * @param {string} appInfo.redirectUri for callback popup
   * @param {string} [appInfo.redirectOrigin] origin of callback window
   * @param {boolean} [appInfo.requireHttps=true] check for https and throw an error if it's omitted
   */
  constructor(appInfo) {
    this.appInfo = Object.assign({}, appInfo); // clone parameters

    if (!appInfo.clientId) {
      throw "appInfo object should have `clientId` property set to your application id";
    }

    if (!appInfo.scopes) {
      throw "appInfo object should have `scopes` property set to the scopes your app needs";
    }

    if (!appInfo.redirectUri) {
      throw "appInfo object should have `redirectUri` property set to your redirect landing url";
    }

    if (!appInfo.redirectOrigin) {
      // get the scheme://host:port from redirectUri
      this.appInfo.redirectOrigin = appInfo.redirectUri.match(/^[\w:]+\/\/[^\/]+/)[0];
    }

    if (typeof appInfo.requireHttps === 'undefined') {
      this.appInfo.requireHttps = true;
    }

    var sep = this.appInfo.redirectUri.indexOf('?') < 0 ? '?' : '&';
    // adds ?clientId=... to the end of string or before the hash in the redirectUri
    // because the auth window doesn't get back any info about client from OneDrive
    this.appInfo.redirectUri = this.appInfo.redirectUri.replace(/(#|$)/, sep + 'clientId=' + this.appInfo.clientId + '$1');
    // list of handlers for successful authorization
    this.callbacks = [];

    // listen for callback's message with auth token
    window.addEventListener('message', this.onAuthenticated.bind(this), false);
  }

  /**
   * Callback to handle successful authorization
   * @callback onAuth
   * @param {string} token
   */

  /**
   * The main auth method. First, we check if we have the user's auth token stored
   * in a cookie. if so, we read it and immediately call your `callback` method.
   * If we can't find the auth cookie, we need to pop up a window and send the user
   * to Microsoft Account so that they can sign in or grant your app the permissions
   * it needs. Depending on whether or not auth() was called in response to a user
   * click, it will either pop up the auth window or display a sign-in button for
   * the user to click (which results in the pop-up). when the user finishes the
   * auth flow, the popup window redirects back to your hosted callback.html file,
   * which calls the onAuthCallback() static method below. It then sets the auth cookie
   * and calls given `callback` function after closing the auth window.
   *
   * Subsequent calls to auth() will usually complete immediately without the
   * popup because the cookie is still fresh.
   *
   * @param {onAuth} callback to call in case of success authorization
   * @param {boolean} [wasClicked=false] whether the call is result of a click or not
   * @return {boolean} if the app is authorized yet; `false` means authorization started
   * @throws if https is required but omitted
   */
  auth(callback, wasClicked) {
    this.ensureHttps();
    var token = this.getTokenFromCookie();
    // if `callback` was missed for whatever reason we transfer its value to `wasClicked`
    wasClicked = wasClicked || (callback === true);
    callback = (typeof callback === 'function') ? callback : null;

    if (token) {
      callback && callback(token);
      return true;
    }

    // would be called in OneDriveAuth.onAuthenticated() method
    callback && this.callbacks.push(callback);

    if (this.inProgress) return false;
    this.inProgress = true;

    if (wasClicked) {
      this.challengeForAuth();
    }
    return false;
  }

  /**
   * Check if current page loaded via HTTPS
   * @return {boolean}
   */
  static isHttps() {
    return window.location.protocol.toLowerCase() === "https:";
  }

  /**
   * For added security we check for https
   * @throws if https is required but omitted
   */
  ensureHttps() {
    if (this.appInfo.requireHttps && !this.isHttps()) {
      throw new Error("HTTPS is required to authorize this application for OneDrive");
    }
  }

  getTokenFromCookie() {
    var cookies = document.cookie;
    var name = "odauth=";
    var start = cookies.indexOf(name);
    if (start >= 0) {
      start += name.length;
      var end = cookies.indexOf(';', start);
      if (end < 0) {
        end = cookies.length;
      }

      var value = cookies.substring(start, end);
      return value;
    }

    return "";
  }

  /**
   * Send user to the Microsoft Account OAuth2.0 page to authorize the app
   * and return the user to your redirectUri.
   */
  challengeForAuth() {
    var appInfo = this.appInfo;
    var url =
      "https://login.live.com/oauth20_authorize.srf" +
      "?client_id=" + appInfo.clientId +
      "&scope=" + encodeURIComponent(appInfo.scopes) +
      "&response_type=token" +
      "&redirect_uri=" + encodeURIComponent(appInfo.redirectUri);
    this.popup(url);
  }

  popup(url) {
    var width = 525,
      height = 525,
      screenX = window.screenX,
      screenY = window.screenY,
      outerWidth = window.outerWidth,
      outerHeight = window.outerHeight;

    var left = screenX + Math.max(outerWidth - width, 0) / 2;
    var top = screenY + Math.max(outerHeight - height, 0) / 2;

    var features = [
      "width=" + width,
      "height=" + height,
      "top=" + top,
      "left=" + left,
      "status=no",
      "resizable=yes",
      "toolbar=no",
      "menubar=no",
      "scrollbars=yes"
    ];
    var popup = window.open(url, "oauth", features.join(","));
    if (!popup) {
      console.error("failed to pop up auth window");
    }

    popup.focus();
  }

  /**
   * Called from auth window in response to successful authorization
   * @param {Event} event object with sent data
   * @param {string} event.data.token
   * @param {string} event.data.clientId
   * @param {string} event.origin origin of callback popup window
   */
  onAuthenticated(event) {
    var callback, data = event.data, token = data.token;
    // skip the message addressed to another client or came from unknown location
    if (this.appInfo.clientId !== data.clientId) return false;
    if (this.appInfo.redirectOrigin !== event.origin) return false;

    this.inProgress = false;

    while (callback = this.callbacks.shift()) {
      callback(token);
    }
  }

  /**
   * Called from the callback page after OAuth authorization.
   * On success it save the token to cookie and call onAuthenticated() method
   * of corresponding OneDriveAuth instance in parent window.
   */
  static onAuthCallback() {
    var authInfo = this.getAuthInfoFromUrl();
    var token = authInfo["access_token"];
    var expiry = parseInt(authInfo["expires_in"]);
    var clientId = authInfo["clientId"];
    var origin = location.origin;

    this.setCookie(token, expiry);
    window.opener.postMessage({ token, clientId }, origin);
    window.close();
  }

  static getAuthInfoFromUrl() {
    if (window.location.hash) {
      var authResponse = (window.location.search + window.location.hash).substr(1);
      var authInfo = JSON.parse(
        '{"' + authResponse.replace(/[&#]/g, '","').replace(/=/g, '":"') + '"}',
        (key, value) => key === "" ? value : decodeURIComponent(value)
      );
      return authInfo;
    } else {
      console.error("failed to receive auth token");
    }
  }

  static setCookie(token, expiresInSeconds) {
    var expiration = new Date();
    expiration.setTime(expiration.getTime() + expiresInSeconds * 1000);
    var cookie = "odauth=" + token + "; path=/; expires=" + expiration.toUTCString();

    if (this.isHttps()) {
      cookie = cookie + ";secure";
    }

    document.cookie = cookie;
  }
}

export default OneDriveAuth;
<|endoftext|>


<commit_before>const { expect } = require('../setup');
const { delay } = require('../lib/mocha-utils');
const matches = require('../lib/capture-matches');
const stripANSI = require('../lib/ansi-strip');
const cli = require('../lib/cli');
const fs = require('../lib/fs');
const {
	PASSWORD,
	PATH_CLI_CONFIG_JSON
} = require('../lib/env');

describe('Token Commands', () => {
	const help = [
		'Manage access tokens (require username/password)',
		'Usage: particle token <command>',
		'Help:  particle help token <command>',
		'',
		'Commands:',
		'  list    List all access tokens for your account',
		'  revoke  Revoke an access token',
		'  create  Create a new access token',
		'',
		'Global Options:',
		'  -v, --verbose  Increases how much logging to display  [count]',
		'  -q, --quiet    Decreases how much logging to display  [count]'
	];

	before(async () => {
		await cli.setTestProfileAndLogin();
	});

	after(async () => {
		await cli.logout();
		await cli.setDefaultProfile();
	});

	it('Shows `help` content', async () => {
		const { stdout, stderr, exitCode } = await cli.run(['help', 'token']);

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Shows `help` content when run without arguments', async () => {
		const { stdout, stderr, exitCode } = await cli.run('token');

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Shows `help` content when run with `--help` flag', async () => {
		const { stdout, stderr, exitCode } = await cli.run(['token', '--help']);

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Creates a token', async () => {
		const subprocess = cli.run(['token', 'create']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token expires on');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Creates a token with an expiration time', async () => {
		const subprocess = cli.run(['token', 'create', '--expires-in', '3600']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token expires on');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Creates a token that does not expire', async () => {
		const subprocess = cli.run(['token', 'create', '--never-expires']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token never expires');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Revokes a token', async () => {
		let subprocess = cli.run(['token', 'create'], { reject: true });

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout: log } = await subprocess;
		const [, token] = stripANSI(log).split('\n').slice(-2).map(t => t.trim());

		expect(token).to.be.a('string').with.lengthOf.at.least(12);

		subprocess = cli.run(['token', 'revoke', token]);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg] = stripANSI(stdout).split('\n').slice(-1).map(t => t.trim());

		expect(msg).to.include(`successfully deleted ${token}`);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Fails to revoke a token if the CLI is using it', async () => {
		const { access_token: token } = await fs.readJson(PATH_CLI_CONFIG_JSON);
		const { stdout, stderr, exitCode } = await cli.run(['token', 'revoke', token]);

		expect(stdout).to.include(`WARNING: ${token} is this CLI's access token`);
		expect(stdout).to.include('use --force to delete it');
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Revokes the CLI\'s token when `--force` flag is set', async () => {
		const { access_token: token } = await fs.readJson(PATH_CLI_CONFIG_JSON);
		const subprocess = cli.run(['token', 'revoke', token, '--force']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg] = stripANSI(stdout).split('\n').slice(-1).map(t => t.trim());
		const cliConfig = await fs.readJson(PATH_CLI_CONFIG_JSON);

		// TODO (mirande): some ascii code nonsense is preventing the logging of:
		// expect(stdout).to.include(`WARNING: ${token} is this CLI's access token`);
		// expect(stdout).to.include('**forcing**');
		expect(msg).to.include(`successfully deleted ${token}`);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
		expect(cliConfig).have.property('access_token', null);

		await cli.login();
	});

	it('Lists tokens', async () => {
		const subprocess = cli.run(['token', 'list']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const tokens = matches(stripANSI(stdout), / Token:\s{6}(.*)/g);

		expect(tokens).to.have.lengthOf.at.least(3);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);

		// TODO (mirande): always revoke all tokens upon completion?
		// await tokens.reduce((promise, t) => {
		// 	return promise.then(() => {
		// 		console.log('REVOKING:', t);
		// 		return cli.run(['token', 'revoke', t], { input: PASSWORD });
		// 	});
		// }, Promise.resolve());
	});
});

<commit_msg>minor tweaks to improve e2e test reliability<commit_after>const { expect } = require('../setup');
const { delay } = require('../lib/mocha-utils');
const matches = require('../lib/capture-matches');
const stripANSI = require('../lib/ansi-strip');
const cli = require('../lib/cli');
const fs = require('../lib/fs');
const {
	PASSWORD,
	PATH_CLI_CONFIG_JSON
} = require('../lib/env');

describe('Token Commands', () => {
	const help = [
		'Manage access tokens (require username/password)',
		'Usage: particle token <command>',
		'Help:  particle help token <command>',
		'',
		'Commands:',
		'  list    List all access tokens for your account',
		'  revoke  Revoke an access token',
		'  create  Create a new access token',
		'',
		'Global Options:',
		'  -v, --verbose  Increases how much logging to display  [count]',
		'  -q, --quiet    Decreases how much logging to display  [count]'
	];

	before(async () => {
		await cli.setTestProfileAndLogin();
	});

	after(async () => {
		await cli.logout();
		await cli.setDefaultProfile();
	});

	it('Shows `help` content', async () => {
		const { stdout, stderr, exitCode } = await cli.run(['help', 'token']);

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Shows `help` content when run without arguments', async () => {
		const { stdout, stderr, exitCode } = await cli.run('token');

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Shows `help` content when run with `--help` flag', async () => {
		const { stdout, stderr, exitCode } = await cli.run(['token', '--help']);

		expect(stdout).to.equal('');
		expect(stderr.split('\n')).to.include.members(help);
		expect(exitCode).to.equal(0);
	});

	it('Creates a token', async () => {
		const subprocess = cli.run(['token', 'create']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token expires on');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Creates a token with an expiration time', async () => {
		const subprocess = cli.run(['token', 'create', '--expires-in', '3600']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token expires on');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Creates a token that does not expire', async () => {
		const subprocess = cli.run(['token', 'create', '--never-expires']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg, token] = stripANSI(stdout).split('\n').slice(-2).map(t => t.trim());

		expect(msg).to.include('New access token never expires');
		expect(token).to.be.a('string').with.lengthOf.at.least(12);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Revokes a token', async () => {
		let subprocess = cli.run(['token', 'create'], { reject: true });

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout: log } = await subprocess;
		const [, token] = stripANSI(log).split('\n').slice(-2).map(t => t.trim());

		expect(token).to.be.a('string').with.lengthOf.at.least(12);

		subprocess = cli.run(['token', 'revoke', token]);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg] = stripANSI(stdout).split('\n').slice(-1).map(t => t.trim());

		expect(msg).to.include(`successfully deleted ${token}`);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Fails to revoke a token if the CLI is using it', async () => {
		const { access_token: token } = await fs.readJson(PATH_CLI_CONFIG_JSON);
		const { stdout, stderr, exitCode } = await cli.run(['token', 'revoke', token]);

		expect(stdout).to.include(`WARNING: ${token} is this CLI's access token`);
		expect(stdout).to.include('use --force to delete it');
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
	});

	it('Revokes the CLI\'s token when `--force` flag is set', async () => {
		const { access_token: token } = await fs.readJson(PATH_CLI_CONFIG_JSON);
		const subprocess = cli.run(['token', 'revoke', token, '--force']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const [msg] = stripANSI(stdout).split('\n').slice(-1).map(t => t.trim());
		const cliConfig = await fs.readJson(PATH_CLI_CONFIG_JSON);

		// TODO (mirande): some ascii code nonsense is preventing the logging of:
		// expect(stdout).to.include(`WARNING: ${token} is this CLI's access token`);
		// expect(stdout).to.include('**forcing**');
		expect(msg).to.include(`successfully deleted ${token}`);
		expect(stderr).to.equal('');
		expect(exitCode).to.equal(0);
		expect(cliConfig).have.property('access_token', null);

		await cli.login();
	});

	it('Lists tokens', async () => {
		const subprocess = cli.run(['token', 'list']);

		await delay(1000);
		subprocess.stdin.write(PASSWORD);
		subprocess.stdin.end('\n');

		const { stdout, stderr, exitCode } = await subprocess;
		const tokens = matches(stripANSI(stdout), / Token:\s{6}(.*)/g);

		expect(tokens).to.have.lengthOf.at.least(3);
		expect(stderr).to.match(/(?:Checking with the cloud\.\.\.)?/);
		expect(exitCode).to.equal(0);

		// TODO (mirande): always revoke all tokens upon completion?
		// console.log('TOKEN COUNT:', tokens.length);
		// await tokens.reduce((promise, t) => {
		// 	return promise.then(() => {
		// 		console.log('REVOKING:', t);
		// 		return cli.run(['token', 'revoke', t], { input: PASSWORD });
		// 	});
		// }, Promise.resolve());
	});
});

<|endoftext|>


<commit_before>window.optly                = window.optly || {};
window.optly.mrkt           = window.optly.mrkt || {};
window.optly.mrkt.services  = window.optly.mrkt.services || {};
window.optly.mrkt.user      = window.optly.mrkt.user || {};
window.optly.mrkt.errorQ    = window.optly.mrkt.errorQ || [];

window.optly.mrkt.Optly_Q = function(acctData, expData){
  var objectCreateSupported = typeof Object.create === 'function';

  window.optly.PRELOAD = window.optly.PRELOAD || {token: null};

  if(!objectCreateSupported) {
    var ThrowAway = function (a, e) {
      this.acctData = a;
      this.expData = e;
    };

    ThrowAway.prototype = window.optly.mrkt.Optly_Q.prototype;

    if(arguments.length > 0) {
      window.optly.mrkt.user.acctData = acctData;
      window.optly.mrkt.user.expData = expData;
      return new ThrowAway(acctData, expData);
    } else{
      return new ThrowAway();
    }
  }

  if(arguments.length > 0 ){
    //window.optly.PRELOAD.token = acctData.csrf_token;

    return Object.create(window.optly.mrkt.Optly_Q.prototype, {
      acctData: {
        value: window.optly.mrkt.user.acctData = acctData,
      },
      expData: {
        value: window.optly.mrkt.user.expData = expData
      }
    });
  } else {
    var acctCache, expCache;

    return Object.create(window.optly.mrkt.Optly_Q.prototype, {
      acctData: {
        get: function(){return acctCache;},
        set: function(val){
          if(!acctCache){
            //window.optly.PRELOAD.token = val.csrf_token;
            window.optly.mrkt.user.acctData = val;
            acctCache = val;
          }
        }
      },
      expData: {
        get: function(){return expCache;},
        set: function(val){
          if(!expCache){
            window.optly.mrkt.user.expData = val;
            expCache = val;
          }
        }
      }
    });
  }
};

window.optly.mrkt.Optly_Q.prototype = {

  transformQueuedArgs: function(queuedArgs) {
    var funcArgs = [];
    $.each(queuedArgs, function(index, arg) {
      if (this[ arg ] !== undefined && typeof arg !== 'object') {
        funcArgs.push(this[ arg ]);
      } else if(typeof arg === 'object') {
        funcArgs.push(arg);
      }
    }.bind(this));

    return funcArgs;
  },

  parseQ: function(fnQ) {
    var queuedArgs,
      transformedArgs,
      queuedFn = fnQ[0];

    queuedArgs = fnQ.slice(1);

    //if there are no arguments call the function with the object scope
    if(queuedArgs.length === 0) {
      queuedFn.call(this);
    } else {
      transformedArgs = this.transformQueuedArgs(queuedArgs);
      queuedFn.apply( queuedFn, transformedArgs );
    }

  },

  push: function(fnQ) {

    switch(typeof fnQ[0]) {
      case 'function':
        this.parseQ(fnQ);
      break;

      case 'string':
        fnQ[0] = this.fnCache[ fnQ[0] ];
        this.parseQ(fnQ);
      break;

      default:
        for (var i = 0; i < fnQ.length; i += 1) {
          if(typeof fnQ[i][0] === 'function') {
            this.parseQ(fnQ[i]);
          } else if(typeof fnQ[i][0] === 'string') {
            fnQ[i][0] = this.fnCache[ fnQ[i][0] ];
            this.parseQ(fnQ[i]);
          }
        }
      break;
    }
  }

};

window.optly.mrkt.services.xhr = {
  makeRequest: function(request) {
    var deferreds = [], deferredPromise;

    // check if multiple requests are present
    if ( $.isArray(request) ) {
      for (var i = 0; i < request.length; i += 1) {
        if (typeof request[i] === 'object') {
          deferredPromise = $.ajax({
            type: request[i].type,
            url: request[i].url,
            xhrFields: request[i].xhrFields ? request[i].xhrFields : {}
          });
          // parameters passed must be objects with a path and properties keys
          if (request[i].properties !== undefined) {
            this.handleErrors( deferredPromise, request[i].url, request[i].properties );
          }

          deferreds.push( deferredPromise );

        }
      }
      this.resolveDeferreds(deferreds);
      return deferreds;
    }
    // If single request, then return the promise directly
    else {
      deferredPromise = $.ajax({
        type: request.type,
        url: request.url,
        xhrFields: request.xhrFields ? request.xhrFields : {}
      });
      if (request.properties !== undefined) {
        this.handleErrors( deferredPromise, request.url, request.properties );
      }
      return deferredPromise;
    }
  },

  logSegmentError: function(category, action, label) {
    window.analytics.ready(function() {
      window.analytics.track(action, {
        category: category,
        label: label
      }, {
        integrations: {
          Marketo: false
        }
      });
    });
  },

  validateTypes: function(resp, properties, apiEndpoint) {
    var errorMessage;
    $.each(properties, function(property, type) {
      // if property is not nested
      if(typeof type !== 'object') {
        if (typeof resp[ property ] !== type) {
          // acctData often comes back as null, but JS will report typeof null to be an object
          errorMessage = 'resp.' + property + ' is not a ' + type + ': ' + ( resp[property] === null ? 'null' : typeof(resp[property]) );

          this.logSegmentError('api error', apiEndpoint, errorMessage);
        }
      }
      // if property is nested
      else {

        this.validateNestedTypes( resp[ property ], properties[ property ], property, type, apiEndpoint );

      }
    }.bind(this)); // end outer .each
  },

  validateNestedTypes: function(data, nestedData, parentProperty, type, apiEndpoint) {
    var errorMessage,
      propertyType;
    // if the property maps to an array
    if ( $.isArray(data) ) {

      $.each(data, function(index, innerProperties) {

        $.each(nestedData, function(innerProp, innerType) {
          propertyType = typeof innerProperties[ innerProp ];
          if (propertyType !== innerType) {
            errorMessage = 'resp.' + parentProperty + '.' + innerProp + ' is not a ' + innerType + ': ' + propertyType;

            this.logSegmentError('api error', apiEndpoint, errorMessage);
          }
        }.bind(this));

      }.bind(this));
    }
    // if the property maps to an object
    else {
      $.each(type, function(innerProp, innerType) {
        propertyType = typeof data[ innerProp ];
        if (propertyType !== innerType) {
          errorMessage = 'resp.' + parentProperty + '.' + innerProp + ' is not a ' + innerType + ': ' + propertyType;

          this.logSegmentError('api error', apiEndpoint, errorMessage);
        }
      }.bind(this));
    }
  },

  handleErrors: function(deferred, apiEndpoint, properties) {
    var parsedRes,
      errorMessage,
      isExpEndpoint = /\/experiment/.test(apiEndpoint);

    deferred.always(function(data, textStatus, jqXHR) {

        // check if the last argument is a promise, if so the response was successful
        if( this.isPromise(jqXHR) && jqXHR.status === 200 ) {

          //parse JSON and catch any errors -- if error return immediately
          try {
            parsedRes = JSON.parse(jqXHR.responseText);
          } catch (error) {
              if(!isExpEndpoint) {
                window.optly.mrkt.errorQ.push([
                  'logError',
                  {
                    error: 'There was an error in your account data.',
                  }
                ]);
              }
              this.logSegmentError('api error', apiEndpoint, 'response contains invalid json ' + error);

              // do not check validations if parse error
              return undefined;
          }

          // validate each property type
          this.validateTypes(parsedRes, properties, apiEndpoint);

        }
        // if the http request fails the jqXHR object will not be promise
        else {
          // in this case the data object is a promise so we parse it's response text
          if ( this.isPromise(data) ) {
            try {
              parsedRes = JSON.parse(data.responseText);
            } catch (error) {
                errorMessage = error + ', Response Text: ' + data.responseText + ', Status Text: ' + data.statusText + ', Status: ' + data.status;
                //if it's staging don't open the error modal because cookie is not being dropped
                if( !isExpEndpoint && !window.customApiDomain) {
                  window.optly.mrkt.errorQ.push([
                    'logError',
                    {
                      error: 'There was an error in your account data.',
                    }
                  ]);
                }
            }

            if (errorMessage === undefined && data.status !== 200) {
              if(parsedRes && parsedRes.id) {
                delete parsedRes.id;
                parsedRes = JSON.stringify(parsedRes);
              } else {
                parsedRes = data.responseText;
              }
              errorMessage = 'Response Text: ' + parsedRes + ', Status Text: ' + data.statusText + ', Status: ' + data.status;
              if(!isExpEndpoint) {
                window.optly.mrkt.errorQ.push([
                  'logError',
                  {
                  error: JSON.parse(data.responseText).error ? JSON.parse(data.responseText).error : 'We were unable to retrieve your account information.',
                  'error_id': JSON.parse(data.responseText).id
                  }
                ]);
              }
            }

            this.logSegmentError('api error', apiEndpoint, errorMessage);

          }

        }

    }.bind(this));

  },

  isPromise: function(value) {

    if (typeof value.then !== 'function') {
        return false;
    }
    var promiseThenSrc = String($.Deferred().then);
    var valueThenSrc = String(value.then);
    return promiseThenSrc === valueThenSrc;
  },

  resolveDeferreds: function(deferreds) {
    var responses = [], oldQue;
    $.when.apply($, deferreds).then(function() {
      // get all arguments returned from done
      var tranformedArgs = Array.prototype.slice.call(arguments);
      $.each(tranformedArgs, function(index, resp) {
        var respData = resp[0];
        if( !this.isPromise( respData ) && resp[1] === 'success' ) {
          responses.push(respData);
        }
        if (index === tranformedArgs.length - 1) {
          oldQue = window.optly_q;

          window.optly_q = window.optly.mrkt.Optly_Q(responses[0], responses[1]);
          window.optly_q.push(oldQue);
        }
      }.bind(this) );
    }.bind(this), function() {

      deferreds[0].then(function(acctData) {
        // check if acctData properties are not null
        if(!!acctData.account_id && !!acctData.email) {
          //if there is no error in account info instantiate the Q with no exp data
          oldQue = window.optly_q;

          window.optly_q = window.optly.mrkt.Optly_Q(acctData);
          window.optly_q.expDataError = true;
          window.optly_q.push(oldQue);
        } else {
          //TODO dfox-powell find way to delete the signed in cookie, potentially load jQuery cookie first Fri Dec  5 16:03:20 2014
          //if acctData is null instantiate the Q with no arguments and remove optimizely signed in cookie
          window.optly_q = window.optly.mrkt.Optly_Q();
        }
      }.bind(this), function() {
        //if acctData error instantiate the Q with no arguments
        window.optly_q = window.optly.mrkt.Optly_Q();
      });
    }.bind(this) );

    return true;
  },

  readCookie: function (name) {
    name = name.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');

    var regex = new RegExp('(?:^|;)\\s?' + name + '=(.*?)(?:;|$)','i'),
        match = document.cookie.match(regex);

    return match && window.unescape(match[1]);
  },

  getLoginStatus: function(requestParams) {
    var tld = window.location.hostname.split('.').pop();
    var ccTLD = [
      'es',
      'fr',
      'de',
      'jp'
    ];
    var isCcTld = ccTLD.indexOf(tld) !== -1;
    var deferreds;

    if ( !!this.readCookie('optimizely_signed_in') || /^www.optimizelystaging.com/.test(window.location.hostname) || isCcTld ) {
      deferreds = this.makeRequest(requestParams);
    } else {
      window.optly_q = window.optly.mrkt.Optly_Q();
    }

    return deferreds;
  }

};

(function() {
  'use strict';

  var acctParams,
    expParams;

  acctParams = {
    type: 'GET',
    url: window.apiDomain + '/account/info',
    properties: {
      email: 'string',
      account_id: 'number'
    },
    xhrFields: {
      withCredentials: true
    }
  };

  expParams = {
    type: 'GET',
    url: window.apiDomain + '/experiment/load_recent?max_experiments=5',
    properties: {
      experiments: {
        id: 'number',
        description: 'string',
        has_started: 'boolean',
        can_edit: 'boolean'
      }
    },
    xhrFields: {
      withCredentials: true
    }
  };

  window.optly.mrkt.services.xhr.getLoginStatus([acctParams, expParams]);
}());
<commit_msg>try removing optimizelystaging regex for account info request<commit_after>window.optly                = window.optly || {};
window.optly.mrkt           = window.optly.mrkt || {};
window.optly.mrkt.services  = window.optly.mrkt.services || {};
window.optly.mrkt.user      = window.optly.mrkt.user || {};
window.optly.mrkt.errorQ    = window.optly.mrkt.errorQ || [];

window.optly.mrkt.Optly_Q = function(acctData, expData){
  var objectCreateSupported = typeof Object.create === 'function';

  window.optly.PRELOAD = window.optly.PRELOAD || {token: null};

  if(!objectCreateSupported) {
    var ThrowAway = function (a, e) {
      this.acctData = a;
      this.expData = e;
    };

    ThrowAway.prototype = window.optly.mrkt.Optly_Q.prototype;

    if(arguments.length > 0) {
      window.optly.mrkt.user.acctData = acctData;
      window.optly.mrkt.user.expData = expData;
      return new ThrowAway(acctData, expData);
    } else{
      return new ThrowAway();
    }
  }

  if(arguments.length > 0 ){
    //window.optly.PRELOAD.token = acctData.csrf_token;

    return Object.create(window.optly.mrkt.Optly_Q.prototype, {
      acctData: {
        value: window.optly.mrkt.user.acctData = acctData,
      },
      expData: {
        value: window.optly.mrkt.user.expData = expData
      }
    });
  } else {
    var acctCache, expCache;

    return Object.create(window.optly.mrkt.Optly_Q.prototype, {
      acctData: {
        get: function(){return acctCache;},
        set: function(val){
          if(!acctCache){
            //window.optly.PRELOAD.token = val.csrf_token;
            window.optly.mrkt.user.acctData = val;
            acctCache = val;
          }
        }
      },
      expData: {
        get: function(){return expCache;},
        set: function(val){
          if(!expCache){
            window.optly.mrkt.user.expData = val;
            expCache = val;
          }
        }
      }
    });
  }
};

window.optly.mrkt.Optly_Q.prototype = {

  transformQueuedArgs: function(queuedArgs) {
    var funcArgs = [];
    $.each(queuedArgs, function(index, arg) {
      if (this[ arg ] !== undefined && typeof arg !== 'object') {
        funcArgs.push(this[ arg ]);
      } else if(typeof arg === 'object') {
        funcArgs.push(arg);
      }
    }.bind(this));

    return funcArgs;
  },

  parseQ: function(fnQ) {
    var queuedArgs,
      transformedArgs,
      queuedFn = fnQ[0];

    queuedArgs = fnQ.slice(1);

    //if there are no arguments call the function with the object scope
    if(queuedArgs.length === 0) {
      queuedFn.call(this);
    } else {
      transformedArgs = this.transformQueuedArgs(queuedArgs);
      queuedFn.apply( queuedFn, transformedArgs );
    }

  },

  push: function(fnQ) {

    switch(typeof fnQ[0]) {
      case 'function':
        this.parseQ(fnQ);
      break;

      case 'string':
        fnQ[0] = this.fnCache[ fnQ[0] ];
        this.parseQ(fnQ);
      break;

      default:
        for (var i = 0; i < fnQ.length; i += 1) {
          if(typeof fnQ[i][0] === 'function') {
            this.parseQ(fnQ[i]);
          } else if(typeof fnQ[i][0] === 'string') {
            fnQ[i][0] = this.fnCache[ fnQ[i][0] ];
            this.parseQ(fnQ[i]);
          }
        }
      break;
    }
  }

};

window.optly.mrkt.services.xhr = {
  makeRequest: function(request) {
    var deferreds = [], deferredPromise;

    // check if multiple requests are present
    if ( $.isArray(request) ) {
      for (var i = 0; i < request.length; i += 1) {
        if (typeof request[i] === 'object') {
          deferredPromise = $.ajax({
            type: request[i].type,
            url: request[i].url,
            xhrFields: request[i].xhrFields ? request[i].xhrFields : {}
          });
          // parameters passed must be objects with a path and properties keys
          if (request[i].properties !== undefined) {
            this.handleErrors( deferredPromise, request[i].url, request[i].properties );
          }

          deferreds.push( deferredPromise );

        }
      }
      this.resolveDeferreds(deferreds);
      return deferreds;
    }
    // If single request, then return the promise directly
    else {
      deferredPromise = $.ajax({
        type: request.type,
        url: request.url,
        xhrFields: request.xhrFields ? request.xhrFields : {}
      });
      if (request.properties !== undefined) {
        this.handleErrors( deferredPromise, request.url, request.properties );
      }
      return deferredPromise;
    }
  },

  logSegmentError: function(category, action, label) {
    window.analytics.ready(function() {
      window.analytics.track(action, {
        category: category,
        label: label
      }, {
        integrations: {
          Marketo: false
        }
      });
    });
  },

  validateTypes: function(resp, properties, apiEndpoint) {
    var errorMessage;
    $.each(properties, function(property, type) {
      // if property is not nested
      if(typeof type !== 'object') {
        if (typeof resp[ property ] !== type) {
          // acctData often comes back as null, but JS will report typeof null to be an object
          errorMessage = 'resp.' + property + ' is not a ' + type + ': ' + ( resp[property] === null ? 'null' : typeof(resp[property]) );

          this.logSegmentError('api error', apiEndpoint, errorMessage);
        }
      }
      // if property is nested
      else {

        this.validateNestedTypes( resp[ property ], properties[ property ], property, type, apiEndpoint );

      }
    }.bind(this)); // end outer .each
  },

  validateNestedTypes: function(data, nestedData, parentProperty, type, apiEndpoint) {
    var errorMessage,
      propertyType;
    // if the property maps to an array
    if ( $.isArray(data) ) {

      $.each(data, function(index, innerProperties) {

        $.each(nestedData, function(innerProp, innerType) {
          propertyType = typeof innerProperties[ innerProp ];
          if (propertyType !== innerType) {
            errorMessage = 'resp.' + parentProperty + '.' + innerProp + ' is not a ' + innerType + ': ' + propertyType;

            this.logSegmentError('api error', apiEndpoint, errorMessage);
          }
        }.bind(this));

      }.bind(this));
    }
    // if the property maps to an object
    else {
      $.each(type, function(innerProp, innerType) {
        propertyType = typeof data[ innerProp ];
        if (propertyType !== innerType) {
          errorMessage = 'resp.' + parentProperty + '.' + innerProp + ' is not a ' + innerType + ': ' + propertyType;

          this.logSegmentError('api error', apiEndpoint, errorMessage);
        }
      }.bind(this));
    }
  },

  handleErrors: function(deferred, apiEndpoint, properties) {
    var parsedRes,
      errorMessage,
      isExpEndpoint = /\/experiment/.test(apiEndpoint);

    deferred.always(function(data, textStatus, jqXHR) {

        // check if the last argument is a promise, if so the response was successful
        if( this.isPromise(jqXHR) && jqXHR.status === 200 ) {

          //parse JSON and catch any errors -- if error return immediately
          try {
            parsedRes = JSON.parse(jqXHR.responseText);
          } catch (error) {
              if(!isExpEndpoint) {
                window.optly.mrkt.errorQ.push([
                  'logError',
                  {
                    error: 'There was an error in your account data.',
                  }
                ]);
              }
              this.logSegmentError('api error', apiEndpoint, 'response contains invalid json ' + error);

              // do not check validations if parse error
              return undefined;
          }

          // validate each property type
          this.validateTypes(parsedRes, properties, apiEndpoint);

        }
        // if the http request fails the jqXHR object will not be promise
        else {
          // in this case the data object is a promise so we parse it's response text
          if ( this.isPromise(data) ) {
            try {
              parsedRes = JSON.parse(data.responseText);
            } catch (error) {
                errorMessage = error + ', Response Text: ' + data.responseText + ', Status Text: ' + data.statusText + ', Status: ' + data.status;
                //if it's staging don't open the error modal because cookie is not being dropped
                if( !isExpEndpoint && !window.customApiDomain) {
                  window.optly.mrkt.errorQ.push([
                    'logError',
                    {
                      error: 'There was an error in your account data.',
                    }
                  ]);
                }
            }

            if (errorMessage === undefined && data.status !== 200) {
              if(parsedRes && parsedRes.id) {
                delete parsedRes.id;
                parsedRes = JSON.stringify(parsedRes);
              } else {
                parsedRes = data.responseText;
              }
              errorMessage = 'Response Text: ' + parsedRes + ', Status Text: ' + data.statusText + ', Status: ' + data.status;
              if(!isExpEndpoint) {
                window.optly.mrkt.errorQ.push([
                  'logError',
                  {
                  error: JSON.parse(data.responseText).error ? JSON.parse(data.responseText).error : 'We were unable to retrieve your account information.',
                  'error_id': JSON.parse(data.responseText).id
                  }
                ]);
              }
            }

            this.logSegmentError('api error', apiEndpoint, errorMessage);

          }

        }

    }.bind(this));

  },

  isPromise: function(value) {

    if (typeof value.then !== 'function') {
        return false;
    }
    var promiseThenSrc = String($.Deferred().then);
    var valueThenSrc = String(value.then);
    return promiseThenSrc === valueThenSrc;
  },

  resolveDeferreds: function(deferreds) {
    var responses = [], oldQue;
    $.when.apply($, deferreds).then(function() {
      // get all arguments returned from done
      var tranformedArgs = Array.prototype.slice.call(arguments);
      $.each(tranformedArgs, function(index, resp) {
        var respData = resp[0];
        if( !this.isPromise( respData ) && resp[1] === 'success' ) {
          responses.push(respData);
        }
        if (index === tranformedArgs.length - 1) {
          oldQue = window.optly_q;

          window.optly_q = window.optly.mrkt.Optly_Q(responses[0], responses[1]);
          window.optly_q.push(oldQue);
        }
      }.bind(this) );
    }.bind(this), function() {

      deferreds[0].then(function(acctData) {
        // check if acctData properties are not null
        if(!!acctData.account_id && !!acctData.email) {
          //if there is no error in account info instantiate the Q with no exp data
          oldQue = window.optly_q;

          window.optly_q = window.optly.mrkt.Optly_Q(acctData);
          window.optly_q.expDataError = true;
          window.optly_q.push(oldQue);
        } else {
          //TODO dfox-powell find way to delete the signed in cookie, potentially load jQuery cookie first Fri Dec  5 16:03:20 2014
          //if acctData is null instantiate the Q with no arguments and remove optimizely signed in cookie
          window.optly_q = window.optly.mrkt.Optly_Q();
        }
      }.bind(this), function() {
        //if acctData error instantiate the Q with no arguments
        window.optly_q = window.optly.mrkt.Optly_Q();
      });
    }.bind(this) );

    return true;
  },

  readCookie: function (name) {
    name = name.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');

    var regex = new RegExp('(?:^|;)\\s?' + name + '=(.*?)(?:;|$)','i'),
        match = document.cookie.match(regex);

    return match && window.unescape(match[1]);
  },

  getLoginStatus: function(requestParams) {
    var tld = window.location.hostname.split('.').pop();
    var ccTLD = [
      'es',
      'fr',
      'de',
      'jp'
    ];
    var isCcTld = ccTLD.indexOf(tld) !== -1;
    var deferreds;

    if ( !!this.readCookie('optimizely_signed_in') || isCcTld ) {
      deferreds = this.makeRequest(requestParams);
    } else {
      window.optly_q = window.optly.mrkt.Optly_Q();
    }

    return deferreds;
  }

};

(function() {
  'use strict';

  var acctParams,
    expParams;

  acctParams = {
    type: 'GET',
    url: window.apiDomain + '/account/info',
    properties: {
      email: 'string',
      account_id: 'number'
    },
    xhrFields: {
      withCredentials: true
    }
  };

  expParams = {
    type: 'GET',
    url: window.apiDomain + '/experiment/load_recent?max_experiments=5',
    properties: {
      experiments: {
        id: 'number',
        description: 'string',
        has_started: 'boolean',
        can_edit: 'boolean'
      }
    },
    xhrFields: {
      withCredentials: true
    }
  };

  window.optly.mrkt.services.xhr.getLoginStatus([acctParams, expParams]);
}());
<|endoftext|>


<commit_before>exports.individu = {
    indemnites_journalieres_maternite: 'indJourMaternite',
    indemnites_journalieres_maladie: 'indJourMaladie',
    indemnites_journalieres_maladie_professionnelle: 'indJourMaladieProf',
    indemnites_journalieres_accident_travail: 'indJourAccidentDuTravail',
    indemnites_volontariat: 'indVolontariat',
    indemnites_chomage_partiel: 'indChomagePartiel',
    chonet: 'allocationsChomage',
    chobrut: {
        src: 'allocationsChomage',
        fn: function(value) {
            return value / (1 - (((0.062 + 0.005) * 0.9825) + 0.028));
        }
    },
    salnet: 'revenusSalarie',
    salbrut: {
        src: 'revenusSalarie',
        fn: function(value) { return value / 0.77; }
    },
    rstnet: 'pensionsRetraitesRentes',
    rstbrut: {
        src: 'pensionsRetraitesRentes',
        fn: function(value) {
            // approximation prélèvement moyen de 7.4 % de cotisations sociales (csg-crds)
            return value / 0.926;
        }
    },
    aah: 'aah',
    pensions_alimentaires_percues: 'pensionsAlimentaires',
    revenus_locatifs: 'revenusLocatifs',
    revenus_capital: 'revenusDuCapital',
    indemnites_stage: 'stage',
    revenus_stage_formation_pro: 'revenusStageFormationPro',
    allocation_securisation_professionnelle: 'allocationSecurisationPro',
    prime_forfaitaire_mensuelle_reprise_activite: 'primeRepriseActivite',
    dedommagement_victime_amiante: 'dedommagementAmiante',
    prestation_compensatoire: 'prestationCompensatoire',
    retraite_combattant: 'retraiteCombattant',
    pensions_invalidite: 'pensionsInvalidite',
    bourse_enseignement_sup: 'bourseEnseignementSup',
    bourse_recherche: 'bourseRecherche',
    gains_exceptionnels: 'gainsExceptionnels',

    // ressourcesYearMoins2Captured
    sal: 'rncRevenusActivite',
    cho: 'rncAutresRevenus',
    rst: 'rncPensionsRetraitesRentes',
    alr: 'rncPensionsAlimentaires'
};

exports.famille = {
    // aspa: 'aspa',
    // aide_logement: 'allocationLogement'
};

exports.foyerFiscal = {
    // Empty
};
<commit_msg>injection salaire_de_base plutôt que salbrut dans openfisca<commit_after>exports.individu = {
    indemnites_journalieres_maternite: 'indJourMaternite',
    indemnites_journalieres_maladie: 'indJourMaladie',
    indemnites_journalieres_maladie_professionnelle: 'indJourMaladieProf',
    indemnites_journalieres_accident_travail: 'indJourAccidentDuTravail',
    indemnites_volontariat: 'indVolontariat',
    indemnites_chomage_partiel: 'indChomagePartiel',
    chonet: 'allocationsChomage',
    chobrut: {
        src: 'allocationsChomage',
        fn: function(value) {
            return value / (1 - (((0.062 + 0.005) * 0.9825) + 0.028));
        }
    },
    salnet: 'revenusSalarie',
    salaire_de_base: {
        src: 'revenusSalarie',
        fn: function(value) { return value / 0.77; }
    },
    rstnet: 'pensionsRetraitesRentes',
    rstbrut: {
        src: 'pensionsRetraitesRentes',
        fn: function(value) {
            // approximation prélèvement moyen de 7.4 % de cotisations sociales (csg-crds)
            return value / 0.926;
        }
    },
    aah: 'aah',
    pensions_alimentaires_percues: 'pensionsAlimentaires',
    revenus_locatifs: 'revenusLocatifs',
    revenus_capital: 'revenusDuCapital',
    indemnites_stage: 'stage',
    revenus_stage_formation_pro: 'revenusStageFormationPro',
    allocation_securisation_professionnelle: 'allocationSecurisationPro',
    prime_forfaitaire_mensuelle_reprise_activite: 'primeRepriseActivite',
    dedommagement_victime_amiante: 'dedommagementAmiante',
    prestation_compensatoire: 'prestationCompensatoire',
    retraite_combattant: 'retraiteCombattant',
    pensions_invalidite: 'pensionsInvalidite',
    bourse_enseignement_sup: 'bourseEnseignementSup',
    bourse_recherche: 'bourseRecherche',
    gains_exceptionnels: 'gainsExceptionnels',

    // ressourcesYearMoins2Captured
    sal: 'rncRevenusActivite',
    cho: 'rncAutresRevenus',
    rst: 'rncPensionsRetraitesRentes',
    alr: 'rncPensionsAlimentaires'
};

exports.famille = {
    // aspa: 'aspa',
    // aide_logement: 'allocationLogement'
};

exports.foyerFiscal = {
    // Empty
};
<|endoftext|>