<commit_before>/*
This file is part of the Juju GUI, which lets users view and manage Juju
environments within a graphical interface (https://launchpad.net/juju-gui).
Copyright (C) 2015 Canonical Ltd.

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

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
General Public License for more details.

You should have received a copy of the GNU Affero General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

'use strict';

const HeaderSearch = React.createClass({

  propTypes: {
    appState: React.PropTypes.object.isRequired
  },

  /**
    Get the current state of the header search.

    @method getInitialState
    @returns {String} The current state.
  */
  getInitialState: function() {
    return {
      query: this._getSearchQuery(),
      active: this._activeForComponent()
    };
  },

  /**
    Grabs the metadata search query from the app state and returns it.
    @method _getSearchQuery
    @returns {String} The search query.
  */
  _getSearchQuery: function() {
    const current = this.props.appState.current;
    return current.search && current.search.text || '';
  },

  /**
    Based on the current state this will return true or false
    if this component is to be in its active state.

    @method _activeForComponent
  */
  _activeForComponent: function() {
    const state = this.props.appState.current;
    return state.root === 'store' ||
            state.store !== undefined || state.search !== undefined;
  },

  /**
    Update the state when the app state changes.

    @method componentWillReceiveProps
  */
  componentWillReceiveProps: function() {
    // Need to check if there is a change to the state and if it has been
    // cleared (store/search results have been closed) then we also need
    // to deactivate the search box.
    const query = this._getSearchQuery();
    if (this._activeForComponent()) {
      this._openSearch();
      // The UI never needs to open the search box and also clear the text in
      // it. Without this check the entered text would be cleared if there was
      // a rerender while typing (before hitting enter).
      if (query !== '') {
        this.setState({query: query});
      }
    } else {
      this._closeSearch();
      this.setState({query: query});
    }
  },

  componentDidUpdate: function() {
    if (!this.state.active) {
      this.refs.searchInput.blur();
    }
  },

  /**
    Generate the base classes based on the props.

    @method _generateClasses
    @returns {String} The collection of class names.
  */
  _generateClasses: function() {
    return classNames(
      'header-search', {
        'header-search--active': this.state.active
      });
  },

  /**
    Generate the close button classes based on the props.

    @method _closeClasses
    @returns {String} The collection of class names.
  */
  _closeClasses: function() {
    return classNames(
      'header-search__close', {
        hidden: !this.state.active
      });
  },

  /**
    Handle the search input receiving focus.

    @method _handleSearchFocus
  */
  _handleSearchFocus: function() {
    this._openSearch(true);
    if (!this.state.active && !this.state.query) {
      this.props.appState.changeState({
        root: 'store',
        user: null,
        profile: null,
        gui: {
          machines: null,
          inspector: null
        }
      });
    }
  },

  /**
    Open the search box.

    @method _openSearch
    @param {Boolean} inputOpen Whether the input should be open.
  */
  _openSearch: function(inputOpen) {
    this.setState({active: true});
    this.refs.searchInput.focus();
  },

  /**
    Close the search box.

    @method _closeSearch
  */
  _closeSearch: function() {
    this.setState({
      query: '',
      active: false
    });
  },

  /**
    Make search when the search form is submitted.

    @method _handleSubmit
    @param {Object} evt The submit event
  */
  _handleSubmit: function(evt) {
    evt.preventDefault();
    // If the search box is not open then instead of submitting the form the
    // search box should be opened.
    if (!this.state.active) {
      this._openSearch(true);
      return;
    }
    const query = this.state.query.trim();
    this.props.appState.changeState({
      hash: null,
      root: null,
      search: {
        owner: null,
        provides: null,
        requires: null,
        series: null,
        tags: null,
        text: query === '' ? null : query,
        type: null
      },
      store: query === '' ? '' : null
    });
  },

  /**
    Close the header and clear the state when the button is clicked.

    @method _handleClose
  */
  _handleClose: function() {
    this._closeSearch();
    this.props.appState.changeState({
      hash: null,
      root: null,
      store: null,
      search: null
    });
  },

  /**
    Update the state from the value of the query input.

    @method _handleQueryChange
    @param {Object} e The input change event
  */
  _handleQueryChange: function(e) {
    this.setState({query: e.currentTarget.value});
  },

  /**
    Navigate to the store when the button is clicked.

    @method _handleStoreClick
    @param {Object} e The click event
  */
  _handleStoreClick: function(e) {
    this.setState({query: ''});
    this.props.appState.changeState({
      root: 'store'
    });
  },

  render: function() {
    return (

        <form className="header-search__form">
          <button type="submit"
            onClick={this._handleSubmit}
            className="header-search__submit">
            <juju.components.SvgIcon name="search_16"
              size="16" />
          </button>
          <input type="search" name="query"
            className="header-search__input"
            placeholder="Search the store"
            value={this.state.query}
            onChange={this._handleQueryChange}
            onFocus={this._handleSearchFocus}
            style={this.state.inputStyles}
            ref="searchInput"/>
        </form>

            <juju.components.SvgIcon name="search_16"
              size="16" />

          <juju.components.SvgIcon name="close_16"
            size="16" />

    );
  }
});

YUI.add('header-search', function() {
  juju.components.HeaderSearch = HeaderSearch;
}, '0.1.0', { requires: [
  'svg-icon'
]});
<commit_msg>Update HeaderSearch to use es6 class.<commit_after>/*
This file is part of the Juju GUI, which lets users view and manage Juju
environments within a graphical interface (https://launchpad.net/juju-gui).
Copyright (C) 2015 Canonical Ltd.

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

This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero
General Public License for more details.

You should have received a copy of the GNU Affero General Public License along
with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

'use strict';

class HeaderSearch extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      query: this._getSearchQuery(),
      active: this._activeForComponent()
    };
  }

  /**
    Grabs the metadata search query from the app state and returns it.
    @method _getSearchQuery
    @returns {String} The search query.
  */
  _getSearchQuery() {
    const current = this.props.appState.current;
    return current.search && current.search.text || '';
  }

  /**
    Based on the current state this will return true or false
    if this component is to be in its active state.

    @method _activeForComponent
  */
  _activeForComponent() {
    const state = this.props.appState.current;
    return state.root === 'store' ||
            state.store !== undefined || state.search !== undefined;
  }

  /**
    Update the state when the app state changes.

    @method componentWillReceiveProps
  */
  componentWillReceiveProps() {
    // Need to check if there is a change to the state and if it has been
    // cleared (store/search results have been closed) then we also need
    // to deactivate the search box.
    const query = this._getSearchQuery();
    if (this._activeForComponent()) {
      this._openSearch();
      // The UI never needs to open the search box and also clear the text in
      // it. Without this check the entered text would be cleared if there was
      // a rerender while typing (before hitting enter).
      if (query !== '') {
        this.setState({query: query});
      }
    } else {
      this._closeSearch();
      this.setState({query: query});
    }
  }

  componentDidUpdate() {
    if (!this.state.active) {
      this.refs.searchInput.blur();
    }
  }

  /**
    Generate the base classes based on the props.

    @method _generateClasses
    @returns {String} The collection of class names.
  */
  _generateClasses() {
    return classNames(
      'header-search', {
        'header-search--active': this.state.active
      });
  }

  /**
    Generate the close button classes based on the props.

    @method _closeClasses
    @returns {String} The collection of class names.
  */
  _closeClasses() {
    return classNames(
      'header-search__close', {
        hidden: !this.state.active
      });
  }

  /**
    Handle the search input receiving focus.

    @method _handleSearchFocus
  */
  _handleSearchFocus() {
    this._openSearch(true);
    if (!this.state.active && !this.state.query) {
      this.props.appState.changeState({
        root: 'store',
        user: null,
        profile: null,
        gui: {
          machines: null,
          inspector: null
        }
      });
    }
  }

  /**
    Open the search box.

    @method _openSearch
    @param {Boolean} inputOpen Whether the input should be open.
  */
  _openSearch(inputOpen) {
    this.setState({active: true});
    this.refs.searchInput.focus();
  }

  /**
    Close the search box.

    @method _closeSearch
  */
  _closeSearch() {
    this.setState({
      query: '',
      active: false
    });
  }

  /**
    Make search when the search form is submitted.

    @method _handleSubmit
    @param {Object} evt The submit event
  */
  _handleSubmit(evt) {
    evt.preventDefault();
    // If the search box is not open then instead of submitting the form the
    // search box should be opened.
    if (!this.state.active) {
      this._openSearch(true);
      return;
    }
    const query = this.state.query.trim();
    this.props.appState.changeState({
      hash: null,
      root: null,
      search: {
        owner: null,
        provides: null,
        requires: null,
        series: null,
        tags: null,
        text: query === '' ? null : query,
        type: null
      },
      store: query === '' ? '' : null
    });
  }

  /**
    Close the header and clear the state when the button is clicked.

    @method _handleClose
  */
  _handleClose() {
    this._closeSearch();
    this.props.appState.changeState({
      hash: null,
      root: null,
      store: null,
      search: null
    });
  }

  /**
    Update the state from the value of the query input.

    @method _handleQueryChange
    @param {Object} e The input change event
  */
  _handleQueryChange(e) {
    this.setState({query: e.currentTarget.value});
  }

  /**
    Navigate to the store when the button is clicked.

    @method _handleStoreClick
    @param {Object} e The click event
  */
  _handleStoreClick(e) {
    this.setState({query: ''});
    this.props.appState.changeState({
      root: 'store'
    });
  }

  render() {
    return (

        <form className="header-search__form">
          <button type="submit"
            onClick={this._handleSubmit.bind(this)}
            className="header-search__submit">
            <juju.components.SvgIcon name="search_16"
              size="16" />
          </button>
          <input type="search" name="query"
            className="header-search__input"
            placeholder="Search the store"
            value={this.state.query}
            onChange={this._handleQueryChange.bind(this)}
            onFocus={this._handleSearchFocus.bind(this)}
            style={this.state.inputStyles}
            ref="searchInput"/>
        </form>

            <juju.components.SvgIcon name="search_16"
              size="16" />

          <juju.components.SvgIcon name="close_16"
            size="16" />

    );
  }
};

HeaderSearch.propTypes = {
  appState: React.PropTypes.object.isRequired
};

YUI.add('header-search', function() {
  juju.components.HeaderSearch = HeaderSearch;
}, '0.1.0', { requires: [
  'svg-icon'
]});
<|endoftext|>


<commit_before>
/*
 *  Guacamole - Clientless Remote Desktop
 *  Copyright (C) 2010  Michael Jumper
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 */

// Guacamole namespace
var Guacamole = Guacamole || {};

/**
 * Core object providing abstract communication for Guacamole. This object
 * is a null implementation whose functions do nothing. Guacamole applications
 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
 * on this one.
 *
 * @constructor
 * @see Guacamole.HTTPTunnel
 */
Guacamole.Tunnel = function() {

    /**
     * Connect to the tunnel with the given optional data. This data is
     * typically used for authentication. The format of data accepted is
     * up to the tunnel implementation.
     *
     * @param {String} data The data to send to the tunnel when connecting.
     */
    this.connect = function(data) {};

    /**
     * Disconnect from the tunnel.
     */
    this.disconnect = function() {};

    /**
     * Send the given message through the tunnel to the service on the other
     * side. All messages are guaranteed to be received in the order sent.
     *
     * @param {String} message The message to send to the service on the other
     *                         side of the tunnel.
     */
    this.sendMessage = function(message) {};

    /**
     * Fired whenever an error is encountered by the tunnel.
     *
     * @event
     * @param {String} message A human-readable description of the error that
     *                         occurred.
     */
    this.onerror = null;

    /**
     * Fired once for every complete Guacamole instruction received, in order.
     *
     * @event
     * @param {String} opcode The Guacamole instruction opcode.
     * @param {Array} parameters The parameters provided for the instruction,
     *                           if any.
     */
    this.oninstruction = null;

};

/**
 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
 *
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {String} tunnelURL The URL of the HTTP tunneling service.
 */
Guacamole.HTTPTunnel = function(tunnelURL) {

    /**
     * Reference to this HTTP tunnel.
     */
    var tunnel = this;

    var tunnel_uuid;

    var TUNNEL_CONNECT = tunnelURL + "?connect";
    var TUNNEL_READ    = tunnelURL + "?read:";
    var TUNNEL_WRITE   = tunnelURL + "?write:";

    var STATE_IDLE          = 0;
    var STATE_CONNECTED     = 1;
    var STATE_DISCONNECTED  = 2;

    var currentState = STATE_IDLE;

    var POLLING_ENABLED     = 1;
    var POLLING_DISABLED    = 0;

    // Default to polling - will be turned off automatically if not needed
    var pollingMode = POLLING_ENABLED;

    var sendingMessages = false;
    var outputMessageBuffer = "";

    this.sendMessage = function(message) {

        // Do not attempt to send messages if not connected
        if (currentState != STATE_CONNECTED)
            return;

        // Add event to queue, restart send loop if finished.
        outputMessageBuffer += message;
        if (!sendingMessages)
            sendPendingMessages();

    };

    function sendPendingMessages() {

        if (outputMessageBuffer.length > 0) {

            sendingMessages = true;

            var message_xmlhttprequest = new XMLHttpRequest();
            message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
            message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

            // Once response received, send next queued event.
            message_xmlhttprequest.onreadystatechange = function() {
                if (message_xmlhttprequest.readyState == 4)
                    sendPendingMessages();
            }

            message_xmlhttprequest.send(outputMessageBuffer);
            outputMessageBuffer = ""; // Clear buffer

        }
        else
            sendingMessages = false;

    }

    function handleResponse(xmlhttprequest) {

        var interval = null;
        var nextRequest = null;

        var dataUpdateEvents = 0;
        var instructionStart = 0;
        var startIndex = 0;

        function parseResponse() {

            // Do not handle responses if not connected
            if (currentState != STATE_CONNECTED) {

                // Clean up interval if polling
                if (interval != null)
                    clearInterval(interval);

                return;
            }

            // Start next request as soon as possible IF request was successful
            if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
                nextRequest = makeRequest();

            // Parse stream when data is received and when complete.
            if (xmlhttprequest.readyState == 3 ||
                xmlhttprequest.readyState == 4) {

                // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
                if (pollingMode == POLLING_ENABLED) {
                    if (xmlhttprequest.readyState == 3 && interval == null)
                        interval = setInterval(parseResponse, 30);
                    else if (xmlhttprequest.readyState == 4 && interval != null)
                        clearInterval(interval);
                }

                // If canceled, stop transfer
                if (xmlhttprequest.status == 0) {
                    tunnel.disconnect();
                    return;
                }

                // Halt on error during request
                else if (xmlhttprequest.status != 200) {

                    // Get error message (if any)
                    var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
                    if (!message)
                        message = "Internal server error";

                    // Call error handler
                    if (tunnel.onerror) tunnel.onerror(message);

                    // Finish
                    tunnel.disconnect();
                    return;
                }

                var current = xmlhttprequest.responseText;
                var instructionEnd;

                while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {

                    // Start next search at next instruction
                    startIndex = instructionEnd+1;

                    var instruction = current.substr(instructionStart,
                            instructionEnd - instructionStart);

                    instructionStart = startIndex;

                    var opcodeEnd = instruction.indexOf(":");

                    var opcode;
                    var parameters;
                    if (opcodeEnd == -1) {
                        opcode = instruction;
                        parameters = new Array();
                    }
                    else {
                        opcode = instruction.substr(0, opcodeEnd);
                        parameters = instruction.substr(opcodeEnd+1).split(",");
                    }

                    // If we're done parsing, handle the next response.
                    if (opcode.length == 0) {

                        delete xmlhttprequest;
                        if (nextRequest)
                            handleResponse(nextRequest);

                        break;
                    }

                    // Call instruction handler.
                    if (tunnel.oninstruction != null)
                        tunnel.oninstruction(opcode, parameters);
                }

                // Start search at end of string.
                startIndex = current.length;

                delete instruction;
                delete parameters;

            }

        }

        // If response polling enabled, attempt to detect if still
        // necessary (via wrapping parseResponse())
        if (pollingMode == POLLING_ENABLED) {
            xmlhttprequest.onreadystatechange = function() {

                // If we receive two or more readyState==3 events,
                // there is no need to poll.
                if (xmlhttprequest.readyState == 3) {
                    dataUpdateEvents++;
                    if (dataUpdateEvents >= 2) {
                        pollingMode = POLLING_DISABLED;
                        xmlhttprequest.onreadystatechange = parseResponse;
                    }
                }

                parseResponse();
            }
        }

        // Otherwise, just parse
        else
            xmlhttprequest.onreadystatechange = parseResponse;

        parseResponse();

    }

    function makeRequest() {

        // Download self
        var xmlhttprequest = new XMLHttpRequest();
        xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
        xmlhttprequest.send(null);

        return xmlhttprequest;

    }

    this.connect = function(data) {

        // Start tunnel and connect synchronously
        var connect_xmlhttprequest = new XMLHttpRequest();
        connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
        connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        connect_xmlhttprequest.send(data);

        // If failure, throw error
        if (connect_xmlhttprequest.status != 200) {

            var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
            if (!message)
                message = "Internal error";

            throw new Error(message);

        }

        // Get UUID from response
        tunnel_uuid = connect_xmlhttprequest.responseText;

        // Start reading data
        currentState = STATE_CONNECTED;
        handleResponse(makeRequest());

    };

    this.disconnect = function() {
        currentState = STATE_DISCONNECTED;
    };

};

Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
<commit_msg>Working initial implementation of websocket tunnel.<commit_after>
/*
 *  Guacamole - Clientless Remote Desktop
 *  Copyright (C) 2010  Michael Jumper
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 */

// Guacamole namespace
var Guacamole = Guacamole || {};

/**
 * Core object providing abstract communication for Guacamole. This object
 * is a null implementation whose functions do nothing. Guacamole applications
 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
 * on this one.
 *
 * @constructor
 * @see Guacamole.HTTPTunnel
 */
Guacamole.Tunnel = function() {

    /**
     * Connect to the tunnel with the given optional data. This data is
     * typically used for authentication. The format of data accepted is
     * up to the tunnel implementation.
     *
     * @param {String} data The data to send to the tunnel when connecting.
     */
    this.connect = function(data) {};

    /**
     * Disconnect from the tunnel.
     */
    this.disconnect = function() {};

    /**
     * Send the given message through the tunnel to the service on the other
     * side. All messages are guaranteed to be received in the order sent.
     *
     * @param {String} message The message to send to the service on the other
     *                         side of the tunnel.
     */
    this.sendMessage = function(message) {};

    /**
     * Fired whenever an error is encountered by the tunnel.
     *
     * @event
     * @param {String} message A human-readable description of the error that
     *                         occurred.
     */
    this.onerror = null;

    /**
     * Fired once for every complete Guacamole instruction received, in order.
     *
     * @event
     * @param {String} opcode The Guacamole instruction opcode.
     * @param {Array} parameters The parameters provided for the instruction,
     *                           if any.
     */
    this.oninstruction = null;

};

/**
 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
 *
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {String} tunnelURL The URL of the HTTP tunneling service.
 */
Guacamole.HTTPTunnel = function(tunnelURL) {

    /**
     * Reference to this HTTP tunnel.
     */
    var tunnel = this;

    var tunnel_uuid;

    var TUNNEL_CONNECT = tunnelURL + "?connect";
    var TUNNEL_READ    = tunnelURL + "?read:";
    var TUNNEL_WRITE   = tunnelURL + "?write:";

    var STATE_IDLE          = 0;
    var STATE_CONNECTED     = 1;
    var STATE_DISCONNECTED  = 2;

    var currentState = STATE_IDLE;

    var POLLING_ENABLED     = 1;
    var POLLING_DISABLED    = 0;

    // Default to polling - will be turned off automatically if not needed
    var pollingMode = POLLING_ENABLED;

    var sendingMessages = false;
    var outputMessageBuffer = "";

    this.sendMessage = function(message) {

        // Do not attempt to send messages if not connected
        if (currentState != STATE_CONNECTED)
            return;

        // Add event to queue, restart send loop if finished.
        outputMessageBuffer += message;
        if (!sendingMessages)
            sendPendingMessages();

    };

    function sendPendingMessages() {

        if (outputMessageBuffer.length > 0) {

            sendingMessages = true;

            var message_xmlhttprequest = new XMLHttpRequest();
            message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
            message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

            // Once response received, send next queued event.
            message_xmlhttprequest.onreadystatechange = function() {
                if (message_xmlhttprequest.readyState == 4)
                    sendPendingMessages();
            }

            message_xmlhttprequest.send(outputMessageBuffer);
            outputMessageBuffer = ""; // Clear buffer

        }
        else
            sendingMessages = false;

    }

    function handleResponse(xmlhttprequest) {

        var interval = null;
        var nextRequest = null;

        var dataUpdateEvents = 0;
        var instructionStart = 0;
        var startIndex = 0;

        function parseResponse() {

            // Do not handle responses if not connected
            if (currentState != STATE_CONNECTED) {

                // Clean up interval if polling
                if (interval != null)
                    clearInterval(interval);

                return;
            }

            // Start next request as soon as possible IF request was successful
            if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
                nextRequest = makeRequest();

            // Parse stream when data is received and when complete.
            if (xmlhttprequest.readyState == 3 ||
                xmlhttprequest.readyState == 4) {

                // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
                if (pollingMode == POLLING_ENABLED) {
                    if (xmlhttprequest.readyState == 3 && interval == null)
                        interval = setInterval(parseResponse, 30);
                    else if (xmlhttprequest.readyState == 4 && interval != null)
                        clearInterval(interval);
                }

                // If canceled, stop transfer
                if (xmlhttprequest.status == 0) {
                    tunnel.disconnect();
                    return;
                }

                // Halt on error during request
                else if (xmlhttprequest.status != 200) {

                    // Get error message (if any)
                    var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
                    if (!message)
                        message = "Internal server error";

                    // Call error handler
                    if (tunnel.onerror) tunnel.onerror(message);

                    // Finish
                    tunnel.disconnect();
                    return;
                }

                var current = xmlhttprequest.responseText;
                var instructionEnd;

                while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {

                    // Start next search at next instruction
                    startIndex = instructionEnd+1;

                    var instruction = current.substr(instructionStart,
                            instructionEnd - instructionStart);

                    instructionStart = startIndex;

                    var opcodeEnd = instruction.indexOf(":");

                    var opcode;
                    var parameters;
                    if (opcodeEnd == -1) {
                        opcode = instruction;
                        parameters = new Array();
                    }
                    else {
                        opcode = instruction.substr(0, opcodeEnd);
                        parameters = instruction.substr(opcodeEnd+1).split(",");
                    }

                    // If we're done parsing, handle the next response.
                    if (opcode.length == 0) {

                        delete xmlhttprequest;
                        if (nextRequest)
                            handleResponse(nextRequest);

                        break;
                    }

                    // Call instruction handler.
                    if (tunnel.oninstruction != null)
                        tunnel.oninstruction(opcode, parameters);
                }

                // Start search at end of string.
                startIndex = current.length;

                delete instruction;
                delete parameters;

            }

        }

        // If response polling enabled, attempt to detect if still
        // necessary (via wrapping parseResponse())
        if (pollingMode == POLLING_ENABLED) {
            xmlhttprequest.onreadystatechange = function() {

                // If we receive two or more readyState==3 events,
                // there is no need to poll.
                if (xmlhttprequest.readyState == 3) {
                    dataUpdateEvents++;
                    if (dataUpdateEvents >= 2) {
                        pollingMode = POLLING_DISABLED;
                        xmlhttprequest.onreadystatechange = parseResponse;
                    }
                }

                parseResponse();
            }
        }

        // Otherwise, just parse
        else
            xmlhttprequest.onreadystatechange = parseResponse;

        parseResponse();

    }

    function makeRequest() {

        // Download self
        var xmlhttprequest = new XMLHttpRequest();
        xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
        xmlhttprequest.send(null);

        return xmlhttprequest;

    }

    this.connect = function(data) {

        // Start tunnel and connect synchronously
        var connect_xmlhttprequest = new XMLHttpRequest();
        connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
        connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        connect_xmlhttprequest.send(data);

        // If failure, throw error
        if (connect_xmlhttprequest.status != 200) {

            var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
            if (!message)
                message = "Internal error";

            throw new Error(message);

        }

        // Get UUID from response
        tunnel_uuid = connect_xmlhttprequest.responseText;

        // Start reading data
        currentState = STATE_CONNECTED;
        handleResponse(makeRequest());

    };

    this.disconnect = function() {
        currentState = STATE_DISCONNECTED;
    };

};

Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();

/**
 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
 *
 * @constructor
 * @augments Guacamole.Tunnel
 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
 */
Guacamole.WebSocketTunnel = function(tunnelURL) {

    /**
     * Reference to this WebSocket tunnel.
     */
    var tunnel = this;

    /**
     * The WebSocket used by this tunnel.
     */
    var socket = null;

    var TUNNEL_CONNECT = tunnelURL + "?connect";

    var STATE_IDLE          = 0;
    var STATE_CONNECTED     = 1;
    var STATE_DISCONNECTED  = 2;

    var currentState = STATE_IDLE;

    this.sendMessage = function(message) {

        // Do not attempt to send messages if not connected
        if (currentState != STATE_CONNECTED)
            return;

        socket.send(message);

    };

    this.connect = function(data) {

        // Connect socket
        socket = new WebSocket(TUNNEL_CONNECT + "&" + data, "guacamole");

        socket.onopen = function(event) {
            currentState = STATE_CONNECTED;
        };

        socket.onmessage = function(event) {

            var message = event.data;

            var instructions = message.split(";");
            for (var i=0; i<instructions.length; i++) {

                var instruction = instructions[i];

                var opcodeEnd = instruction.indexOf(":");
                if (opcodeEnd == -1)
                    opcodeEnd = instruction.length;

                var opcode = instruction.substring(0, opcodeEnd);
                var parameters = instruction.substring(opcodeEnd+1).split(",");

                if (tunnel.oninstruction)
                    tunnel.oninstruction(opcode, parameters);

            }

        };

    };

    this.disconnect = function() {
        currentState = STATE_DISCONNECTED;
        socket.close();
    };

};

Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();

<|endoftext|>


<commit_before>var JsNumber = {
  /* Alternative approach to the int32 and uint32 conversions
     source: http://www.2ality.com/2012/02/js-integers.html
     function modulo(a, b) {
       return a - Math.floor(a/b)*b;
     }
     function ToUint32(x) {
       return modulo(ToInteger(x), Math.pow(2, 32));
     }

     function ToInt32(x) {
       var uint32 = ToUint32(x);
       if (uint32 >= Math.pow(2, 31)) {
         return uint32 - Math.pow(2, 32)
       } else {
         return uint32;
       }
     }
   */

  // this works because the >>> operator first converts its first argument to uint32
  to_uint32 : function (x) {
    return x >>> 0;
  },

  // this works because the >> operator first converts its first argument to int32
  to_int32 : function (x) {
    return x >> 0;
  },

  from_string : function (x) {
    return Number(x);
  },

  to_string : function (x) {
    return "" + x;
  },

  int32_left_shift : function (x, y) { return x << y; },
  int32_right_shift : function (x, y) { return x >> y; },
  uint32_right_shift : function (x, y) { return x >>> y; },

  int32_bitwise_and : function (x, y) { return x & y; },
  int32_bitwise_or : function (x, y) { return x | y; },
  int32_bitwise_xor : function (x, y) { return x ^ y; },
  int32_bitwise_not : function (x) { return ~ x; },

  floor : function (x) { return Math.floor(x); },
  neg : function (x) { return - x; },
  sign : function (x) { return Math.sign(x); },
  absolute : function (x) { return Math.abs(x); },
  fmod : function (x, y) { return x % y; },

  modulo_32 : function (x) { return x % (1 << 32); }, // TODO check !

  zero : 0.0,
  neg_zero : -0.0,
  one : 1.0,
  infinity : Number.POSITIVE_INFINITY,
  neg_infinity : Number.NEGATIVE_INFINITY,
  max_value : Number.MAX_VALUE,
  min_value : Number.MIN_VALUE,
  nan : Number.NaN,
  pi : Math.PI,
  e : Math.E,
  ln2 : Math.LN2,

  /* TODO: what about other functions from Math? */

};

<commit_msg>tiny tweaks<commit_after>var JsNumber = {
  /* Alternative approach to the int32 and uint32 conversions
     source: http://www.2ality.com/2012/02/js-integers.html
     function modulo(a, b) {
       return a - Math.floor(a/b)*b;
     }
     function ToUint32(x) {
       return modulo(ToInteger(x), Math.pow(2, 32));
     }

     function ToInt32(x) {
       var uint32 = ToUint32(x);
       if (uint32 >= Math.pow(2, 31)) {
         return uint32 - Math.pow(2, 32)
       } else {
         return uint32;
       }
     }
   */

  // this works because the >>> operator first converts its first argument to uint32
  to_uint32 : function (x) {
    return x >>> 0;
  },

  // this works because the >> operator first converts its first argument to int32
  to_int32 : function (x) {
    return x >> 0;
  },

  from_string : function (x) {
    return Number(x);
  },

  to_string : function (x) {
    return "" + x;
  },

  int32_left_shift : function (x, y) { return x << y; },
  int32_right_shift : function (x, y) { return x >> y; },
  uint32_right_shift : function (x, y) { return x >>> y; },

  int32_bitwise_and : function (x, y) { return x & y; },
  int32_bitwise_or : function (x, y) { return x | y; },
  int32_bitwise_xor : function (x, y) { return x ^ y; },
  int32_bitwise_not : function (x) { return ~ x; },

  floor : function (x) { return Math.floor(x); },
  neg : function (x) { return - x; },
  sign : function (x) { return Math.sign(x); },
  absolute : function (x) { return Math.abs(x); },
  fmod : function (x, y) { return x % y; },

  modulo_32 : function (x) { return x % (1 << 32); }, // TODO check !

  zero : 0.0,
  neg_zero : -0.0,
  one : 1.0,
  infinity : Number.POSITIVE_INFINITY,
  neg_infinity : Number.NEGATIVE_INFINITY,
  max_value : Number.MAX_VALUE,
  min_value : Number.MIN_VALUE,
  nan : Number.NaN,
  pi : Math.PI,
  e : Math.E,
  ln2 : Math.LN2,

  /* TODO: what about other functions from Math? */
};
<|endoftext|>


<commit_before>// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

'use strict';

var Q = require('q');
var mongojs = require('./q-mongojs');
var config = require('./config');

var db = mongojs(config.dbUri, ['games', 'percentiles', 'meta']);

db.removeAll = function () {
    return Q.all([
        db.games.remove(),
        db.percentiles.remove()
    ]);
};

function ensureIndexes() {
    return Q.all([
        db.games.ensureIndex({ gameId: 1 }, { unique: true }),
        db.games.ensureIndex({ startTime: 1 }),
        db.percentiles.ensureIndex({ 'value.teamSize': 1, 'value.timepoint': 1 }, { unique: true })
    ]);
}

function getMeta() {
    return db.meta.findOne({ _id: 1 })
        .then(function (meta) {
            return meta ? meta : { _id: 1, revision: 0 };
        })
}

function setMeta(meta) {
    return db.meta.save(meta);
}

function upgrade(rev, fn) {
    return function (meta) {
        if (meta.revision < rev) {
            meta.revision = rev;
            console.info("Upgrading to revision %s", rev);
            return Q.fcall(fn).then(function () {
                return meta;
            });
        }
        return meta;
    }
}

db.init = function () {
    return getMeta()
        .then(function (meta) {
            console.info("Database revision is %s", meta.revision);
            return meta;
        })
        .then(upgrade(20140126, function () {
            // Updater logic was changed. Better re-download everything
            // to make sure that no games are missed.
            return db.games.remove({});
        }))
        .then(upgrade(20140130, function () {
            // Grouping stats by teamSize added. Timepoints are not anymore unique.
            return db.percentiles.dropIndex('value.timepoint_1');
        }))
        .then(upgrade(20140131, function () {
            // Changed to a compound index.
            return db.percentiles.dropIndex('value.timepoint_1');
        }))
        .then(setMeta)
        .then(ensureIndexes);
};

module.exports = db;
<commit_msg>Refactored<commit_after>// Copyright © 2013-2014 Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0

'use strict';

var _ = require('underscore');
var Q = require('q');
var mongojs = require('./q-mongojs');
var config = require('./config');

var db = mongojs(config.dbUri, ['games', 'percentiles', 'meta']);

db.removeAll = function () {
    return Q.all([
        db.games.remove(),
        db.percentiles.remove()
    ]);
};

function ensureIndexes() {
    return Q.all([
        db.games.ensureIndex({ gameId: 1 }, { unique: true }),
        db.games.ensureIndex({ startTime: 1 }),
        db.percentiles.ensureIndex({ 'value.teamSize': 1, 'value.timepoint': 1 }, { unique: true })
    ]);
}

function getMeta() {
    return db.meta.findOne({ _id: 1 })
        .then(function (meta) {
            return meta ? meta : { _id: 1, revision: 0 };
        })
}

function setMeta(meta) {
    return db.meta.save(meta);
}

function upgrade(rev, fn) {
    return function (meta) {
        if (meta.revision < rev) {
            meta.revision = rev;
            console.info("Upgrading to revision %s", rev);
            return Q.fcall(fn).then(function () {
                return meta;
            });
        }
        return meta;
    }
}

function dropIndexes(collection) {
    return collection.indexes()
        .then(function (indexes) {
            return Q.all(_.chain(indexes)
                .pluck('name')
                .filter(function (name) {
                    return name !== '_id_';
                })
                .map(function (name) {
                    return collection.dropIndex(name);
                })
                .value());
        });
}

db.init = function () {
    return getMeta()
        .then(function (meta) {
            console.info("Database revision is %s", meta.revision);
            return meta;
        })
        .then(upgrade(20140126, function () {
            // Updater logic was changed. Better re-download everything
            // to make sure that no games are missed.
            return db.games.remove({});
        }))
        .then(upgrade(20140130, function () {
            // Grouping stats by teamSize added. Timepoints are not anymore unique.
            return dropIndexes(db.percentiles);
        }))
        .then(upgrade(20140131, function () {
            // Changed to a compound index.
            return dropIndexes(db.percentiles);
        }))
        .then(setMeta)
        .then(ensureIndexes);
};

module.exports = db;
<|endoftext|>


<commit_before>const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const { Configuration } = require('@schul-cloud/commons');

const api = require('../api');
const permissionsHelper = require('./permissions');
const wordlist = require('../static/other/wordlist.js');

const { NODE_ENV, SW_ENABLED } = require('../config/global');
const logger = require('./logger');

const rolesDisplayName = {
	teacher: 'Lehrer',
	student: 'Schüler',
	administrator: 'Administrator',
	superhero: 'Schul-Cloud Admin',
	demo: 'Demo',
	demoTeacher: 'Demo',
	demoStudent: 'Demo',
	helpdesk: 'Helpdesk',
	betaTeacher: 'Beta',
	expert: 'Experte',
};

const generatePassword = () => {
	const passphraseParts = [];

	// iterate 3 times, to add 3 password parts
	[1, 2, 3].forEach(() => {
		passphraseParts.push(
			wordlist[crypto.randomBytes(2).readUInt16LE(0) % wordlist.length],
		);
	});
	return passphraseParts.join(' ');
};

const clearCookie = async (req, res, options = { destroySession: false }) => {
	if (options.destroySession && req.session && req.session.destroy) {
		await new Promise((resolve, reject) => {
			req.session.destroy((err) => {
				if (err) {
					reject(err);
					return;
				}
				resolve();
			});
		});
	}
	res.clearCookie('jwt');
	// this is deprecated and only used for cookie removal from now on,
	// and can be removed after one month (max cookie lifetime from life systems)
	if (res.locals && res.locals.domain) {
		res.clearCookie('jwt', { domain: res.locals.domain });
	}
};

const isJWT = (req) => (req && req.cookies && req.cookies.jwt);

const isAuthenticated = (req) => {
	if (!isJWT(req)) {
		return Promise.resolve(false);
	}

	return api(req).post('/authentication', {
		json: {
			strategy: 'jwt',
			accessToken: req.cookies.jwt,
		},
	}).then(() => true).catch(() => false);
};

const populateCurrentUser = (req, res) => {
	let payload = {};
	if (isJWT(req)) {
		try {
			// eslint-disable-next-line prefer-destructuring
			payload = (jwt.decode(req.cookies.jwt, { complete: true }) || {}).payload;
			res.locals.currentPayload = payload;
		} catch (err) {
			logger.error('Broken JWT / JWT decoding failed', { error: err });
			return clearCookie(req, res, { destroySession: true })
				.catch((err) => { logger.error('clearCookie failed during jwt check', { error: err.toString() }); })
				.finally(() => res.redirect('/'));
		}
	}

	// separates users in two groups for AB testing
	function setTestGroup(user) {
		if (SW_ENABLED) {
			const lChar = user._id.substr(user._id.length - 1);
			const group = parseInt(lChar, 16) % 2 ? 1 : 0;
			user.testGroup = group;
		} else {
			user.testGroup = 0;
		}
	}

	if (payload && payload.userId) {
		return api(req).get(`/users/${payload.userId}`, {
			qs: {
				$populate: ['roles'],
			},
		}).then((data) => {
			res.locals.currentUser = data;
			setTestGroup(res.locals.currentUser);
			res.locals.currentRole = rolesDisplayName[data.roles[0].name];
			res.locals.roles = data.roles.map(({ name }) => name);
			res.locals.roleNames = data.roles.map((r) => rolesDisplayName[r.name]);
			return api(req).get(`/schools/${res.locals.currentUser.schoolId}`, {
				qs: {
					$populate: ['federalState'],
				},
			}).then((data2) => {
				res.locals.currentSchool = res.locals.currentUser.schoolId;
				res.locals.currentSchoolData = data2;
				res.locals.currentSchoolData.isExpertSchool = data2.purpose === 'expert';
				return data2;
			});
		}).catch((e) => {
			// 400 for missing information in jwt, 401 for invalid jwt, not-found for deleted user
			if (e.statusCode === 400 || e.statusCode === 401 || e.error.className === 'not-found') {
				return clearCookie(req, res, { destroySession: true })
					.catch((err) => { logger.error('clearCookie failed during populateUser', { error: err.toString() }); })
					.finally(() => res.redirect('/'));
			}
			throw e;
		});
	}

	return Promise.resolve();
};

const checkConsent = (req, res) => {
	if (
		((res.locals.currentUser || {}).preferences || {}).firstLogin	// do not exist if 3. system login
		|| req.path.startsWith('/login/success')
		|| req.baseUrl.startsWith('/firstLogin')) {
		return Promise.resolve();
	}
	// eslint-disable-next-line prefer-promise-reject-errors
	return Promise.reject('firstLogin was not completed, redirecting...');
};

const checkSuperhero = (req, res) => {
	if (!(res.locals.roles || []).includes('superhero')) {
		return Promise.resolve();
	}

	// eslint-disable-next-line prefer-promise-reject-errors
	return Promise.reject('superhero access forbidden, redirecting...');
};

const restrictSidebar = (req, res) => {
	res.locals.sidebarItems = res.locals.sidebarItems.filter((item) => {
		if (!item.permission) return true;

		const hasRequiredPermission = permissionsHelper.userHasPermission(res.locals.currentUser, item.permission);
		const hasExcludedPermission = permissionsHelper.userHasPermission(res.locals.currentUser,
			item.excludedPermission);
		return hasRequiredPermission && !hasExcludedPermission;
		// excludedPermission is used to prevent the case that an Admin has both: Verwaltung and Administration
	});
};

const authChecker = (req, res, next) => {
	isAuthenticated(req)
		.then((isAuthenticated2) => {
			const redirectUrl = Configuration.get('NOT_AUTHENTICATED_REDIRECT_URL');
			if (isAuthenticated2) {
				// fetch user profile
				populateCurrentUser(req, res)
					.then(() => checkSuperhero(req, res))
					.then(() => checkConsent(req, res))
					.then(() => restrictSidebar(req, res))
					.then(() => {
						next();
						return null;
					})
					.catch((err) => {
						if (err === 'firstLogin was not completed, redirecting...') {
							// print message?
							res.redirect('/firstLogin');
						} else if (err === 'superhero access forbidden, redirecting...') {
							res.redirect('/login/superhero');
						} else {
							res.redirect(redirectUrl);
						}
					});
			} else {
				res.redirect(`${redirectUrl}?redirect=${req.originalUrl}`);
			}
		});
};

const login = (payload = {}, req, res, next) => {
	const { redirect } = payload;
	delete payload.redirect;
	return api(req).post('/authentication', { json: payload }).then((data) => {
		res.cookie('jwt', data.accessToken, {
			expires: new Date(Date.now() + Configuration.get('COOKIE__EXPIRES_SECONDS')),
			httpOnly: Configuration.get('COOKIE__HTTP_ONLY'), // can't be set to true with nuxt client
			hostOnly: Configuration.get('COOKIE__HOST_ONLY'),
			sameSite: Configuration.get('COOKIE__SAME_SITE'), // restrict jwt access to our domain ressources only
			secure: Configuration.get('COOKIE__SECURE'),
		});
		let redirectUrl = '/login/success';
		if (redirect) {
			redirectUrl = `${redirectUrl}?redirect=${redirect}`;
		}
		res.redirect(redirectUrl);
	}).catch((e) => {
		res.locals.notification = {
			type: 'danger',
			message: res.$t('login.text.loginFailed'),
			statusCode: e.statusCode,
			timeToWait: Configuration.get('LOGIN_BLOCK_TIME'),
		};
		if (e.statusCode === 429) {
			res.locals.notification.timeToWait = e.error.data.timeToWait;
		}
		next(e);
	});
};

const etherpadCookieHelper = (etherpadSession, padId, res) => {
	const encodedPadId = encodeURI(padId);
	res.cookie('sessionID', etherpadSession.data.sessionID, {
		path: `/p/${encodedPadId}`, //`${Configuration.get('ETHERPAD_BASE_PATH')}/p/${encodedPadId}`,
		expires: new Date(etherpadSession.data.validUntil * 1000),
		httpOnly: Configuration.get('COOKIE__HTTP_ONLY'),
		hostOnly: Configuration.get('COOKIE__HOST_ONLY'),
		sameSite: Configuration.get('COOKIE__SAME_SITE'),
		secure: Configuration.get('COOKIE__SECURE'),
	});
};

module.exports = {
	clearCookie,
	isJWT,
	authChecker,
	isAuthenticated,
	restrictSidebar,
	populateCurrentUser,
	login,
	etherpadCookieHelper,
	generatePassword,
};
<commit_msg>remove debugging code<commit_after>const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const { Configuration } = require('@schul-cloud/commons');

const api = require('../api');
const permissionsHelper = require('./permissions');
const wordlist = require('../static/other/wordlist.js');

const { NODE_ENV, SW_ENABLED } = require('../config/global');
const logger = require('./logger');

const rolesDisplayName = {
	teacher: 'Lehrer',
	student: 'Schüler',
	administrator: 'Administrator',
	superhero: 'Schul-Cloud Admin',
	demo: 'Demo',
	demoTeacher: 'Demo',
	demoStudent: 'Demo',
	helpdesk: 'Helpdesk',
	betaTeacher: 'Beta',
	expert: 'Experte',
};

const generatePassword = () => {
	const passphraseParts = [];

	// iterate 3 times, to add 3 password parts
	[1, 2, 3].forEach(() => {
		passphraseParts.push(
			wordlist[crypto.randomBytes(2).readUInt16LE(0) % wordlist.length],
		);
	});
	return passphraseParts.join(' ');
};

const clearCookie = async (req, res, options = { destroySession: false }) => {
	if (options.destroySession && req.session && req.session.destroy) {
		await new Promise((resolve, reject) => {
			req.session.destroy((err) => {
				if (err) {
					reject(err);
					return;
				}
				resolve();
			});
		});
	}
	res.clearCookie('jwt');
	// this is deprecated and only used for cookie removal from now on,
	// and can be removed after one month (max cookie lifetime from life systems)
	if (res.locals && res.locals.domain) {
		res.clearCookie('jwt', { domain: res.locals.domain });
	}
};

const isJWT = (req) => (req && req.cookies && req.cookies.jwt);

const isAuthenticated = (req) => {
	if (!isJWT(req)) {
		return Promise.resolve(false);
	}

	return api(req).post('/authentication', {
		json: {
			strategy: 'jwt',
			accessToken: req.cookies.jwt,
		},
	}).then(() => true).catch(() => false);
};

const populateCurrentUser = (req, res) => {
	let payload = {};
	if (isJWT(req)) {
		try {
			// eslint-disable-next-line prefer-destructuring
			payload = (jwt.decode(req.cookies.jwt, { complete: true }) || {}).payload;
			res.locals.currentPayload = payload;
		} catch (err) {
			logger.error('Broken JWT / JWT decoding failed', { error: err });
			return clearCookie(req, res, { destroySession: true })
				.catch((err) => { logger.error('clearCookie failed during jwt check', { error: err.toString() }); })
				.finally(() => res.redirect('/'));
		}
	}

	// separates users in two groups for AB testing
	function setTestGroup(user) {
		if (SW_ENABLED) {
			const lChar = user._id.substr(user._id.length - 1);
			const group = parseInt(lChar, 16) % 2 ? 1 : 0;
			user.testGroup = group;
		} else {
			user.testGroup = 0;
		}
	}

	if (payload && payload.userId) {
		return api(req).get(`/users/${payload.userId}`, {
			qs: {
				$populate: ['roles'],
			},
		}).then((data) => {
			res.locals.currentUser = data;
			setTestGroup(res.locals.currentUser);
			res.locals.currentRole = rolesDisplayName[data.roles[0].name];
			res.locals.roles = data.roles.map(({ name }) => name);
			res.locals.roleNames = data.roles.map((r) => rolesDisplayName[r.name]);
			return api(req).get(`/schools/${res.locals.currentUser.schoolId}`, {
				qs: {
					$populate: ['federalState'],
				},
			}).then((data2) => {
				res.locals.currentSchool = res.locals.currentUser.schoolId;
				res.locals.currentSchoolData = data2;
				res.locals.currentSchoolData.isExpertSchool = data2.purpose === 'expert';
				return data2;
			});
		}).catch((e) => {
			// 400 for missing information in jwt, 401 for invalid jwt, not-found for deleted user
			if (e.statusCode === 400 || e.statusCode === 401 || e.error.className === 'not-found') {
				return clearCookie(req, res, { destroySession: true })
					.catch((err) => { logger.error('clearCookie failed during populateUser', { error: err.toString() }); })
					.finally(() => res.redirect('/'));
			}
			throw e;
		});
	}

	return Promise.resolve();
};

const checkConsent = (req, res) => {
	if (
		((res.locals.currentUser || {}).preferences || {}).firstLogin	// do not exist if 3. system login
		|| req.path.startsWith('/login/success')
		|| req.baseUrl.startsWith('/firstLogin')) {
		return Promise.resolve();
	}
	// eslint-disable-next-line prefer-promise-reject-errors
	return Promise.reject('firstLogin was not completed, redirecting...');
};

const checkSuperhero = (req, res) => {
	if (!(res.locals.roles || []).includes('superhero')) {
		return Promise.resolve();
	}

	// eslint-disable-next-line prefer-promise-reject-errors
	return Promise.reject('superhero access forbidden, redirecting...');
};

const restrictSidebar = (req, res) => {
	res.locals.sidebarItems = res.locals.sidebarItems.filter((item) => {
		if (!item.permission) return true;

		const hasRequiredPermission = permissionsHelper.userHasPermission(res.locals.currentUser, item.permission);
		const hasExcludedPermission = permissionsHelper.userHasPermission(res.locals.currentUser,
			item.excludedPermission);
		return hasRequiredPermission && !hasExcludedPermission;
		// excludedPermission is used to prevent the case that an Admin has both: Verwaltung and Administration
	});
};

const authChecker = (req, res, next) => {
	isAuthenticated(req)
		.then((isAuthenticated2) => {
			const redirectUrl = Configuration.get('NOT_AUTHENTICATED_REDIRECT_URL');
			if (isAuthenticated2) {
				// fetch user profile
				populateCurrentUser(req, res)
					.then(() => checkSuperhero(req, res))
					.then(() => checkConsent(req, res))
					.then(() => restrictSidebar(req, res))
					.then(() => {
						next();
						return null;
					})
					.catch((err) => {
						if (err === 'firstLogin was not completed, redirecting...') {
							// print message?
							res.redirect('/firstLogin');
						} else if (err === 'superhero access forbidden, redirecting...') {
							res.redirect('/login/superhero');
						} else {
							res.redirect(redirectUrl);
						}
					});
			} else {
				res.redirect(`${redirectUrl}?redirect=${req.originalUrl}`);
			}
		});
};

const login = (payload = {}, req, res, next) => {
	const { redirect } = payload;
	delete payload.redirect;
	return api(req).post('/authentication', { json: payload }).then((data) => {
		res.cookie('jwt', data.accessToken, {
			expires: new Date(Date.now() + Configuration.get('COOKIE__EXPIRES_SECONDS')),
			httpOnly: Configuration.get('COOKIE__HTTP_ONLY'), // can't be set to true with nuxt client
			hostOnly: Configuration.get('COOKIE__HOST_ONLY'),
			sameSite: Configuration.get('COOKIE__SAME_SITE'), // restrict jwt access to our domain ressources only
			secure: Configuration.get('COOKIE__SECURE'),
		});
		let redirectUrl = '/login/success';
		if (redirect) {
			redirectUrl = `${redirectUrl}?redirect=${redirect}`;
		}
		res.redirect(redirectUrl);
	}).catch((e) => {
		res.locals.notification = {
			type: 'danger',
			message: res.$t('login.text.loginFailed'),
			statusCode: e.statusCode,
			timeToWait: Configuration.get('LOGIN_BLOCK_TIME'),
		};
		if (e.statusCode === 429) {
			res.locals.notification.timeToWait = e.error.data.timeToWait;
		}
		next(e);
	});
};

const etherpadCookieHelper = (etherpadSession, padId, res) => {
	const encodedPadId = encodeURI(padId);
	res.cookie('sessionID', etherpadSession.data.sessionID, {
		path: `${Configuration.get('ETHERPAD_BASE_PATH')}/p/${encodedPadId}`,
		expires: new Date(etherpadSession.data.validUntil * 1000),
		httpOnly: Configuration.get('COOKIE__HTTP_ONLY'),
		hostOnly: Configuration.get('COOKIE__HOST_ONLY'),
		sameSite: Configuration.get('COOKIE__SAME_SITE'),
		secure: Configuration.get('COOKIE__SECURE'),
	});
};

module.exports = {
	clearCookie,
	isJWT,
	authChecker,
	isAuthenticated,
	restrictSidebar,
	populateCurrentUser,
	login,
	etherpadCookieHelper,
	generatePassword,
};
<|endoftext|>


<commit_before>// "C" is for cookie, that's good enough for me.

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1);
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays*24*60*60*1000));
    var expires = "expires="+d.toUTCString();
    document.cookie = cname + "=" + cvalue + "; " + expires + ";domain='pokejisho.com'";
}<commit_msg>added domain to cookies<commit_after>// "C" is for cookie, that's good enough for me.

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1);
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}

function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays*24*60*60*1000));
    var expires = "expires="+d.toUTCString();
    document.cookie = cname + "=" + cvalue + "; " + expires + ";domain=pokejisho.com;path=/";
}<|endoftext|>


<commit_before>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function loadDialog() {
	this.formatInfo = window.arguments[0];
	document.getElementById('format-name').value = this.formatInfo.name;
	var sourceTextbox = document.getElementById('format-source');
	sourceTextbox.value = this.formatInfo.getSource();
	if (!this.formatInfo.saveFormat) {
		// preset format
		sourceTextbox.readonly = true;
		document.getElementById("note").hidden = false;
	}
	if (!this.formatInfo.id) {
		// new format
		document.getElementById("name-box").hidden = false;
	}
}

function saveDialog() {
	if (!this.formatInfo.id) {
		var name = document.getElementById('format-name').value;
		if (name.length > 0) {
			this.formatInfo.name = name;
		} else {
			window.alert("Please specify a name");
			return false;
		}
	}
	this.formatInfo.saveFormat(document.getElementById('format-source').value);
	this.formatInfo.saved = true;
	return true;
}
<commit_msg>jeremy.herault: fixing issue 356 : Just an editable format can be edited and saved, and a readonly format can't be edited, so ... now it work<commit_after>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function loadDialog() {
	this.formatInfo = window.arguments[0];
	document.getElementById('format-name').value = this.formatInfo.name;
	var sourceTextbox = document.getElementById('format-source');
	sourceTextbox.value = this.formatInfo.getSource();
	if (!this.formatInfo.saveFormat) {
		// preset format
		sourceTextbox.setAttribute("readonly","true");
		document.getElementById("note").hidden = false;
	}else{
		sourceTextbox.removeAttribute("readonly");
	}
	if (!this.formatInfo.id) {
		// new format
		document.getElementById("name-box").hidden = false;
	}
}

function saveDialog() {
	if (!this.formatInfo.id) {
		var name = document.getElementById('format-name').value;
		if (name.length > 0) {
			this.formatInfo.name = name;
		} else {
			window.alert("Please specify a name");
			return false;
		}
	}
	//only save an editable format
	if (this.formatInfo.saveFormat){
		this.formatInfo.saveFormat(document.getElementById('format-source').value);
		this.formatInfo.saved = true;
		return true;
	}
}
<|endoftext|>


<commit_before>import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { dasherize } from '@ember/string';
import { startCase } from 'lodash';
import { task } from 'ember-concurrency';
import ENV from '@cardstack/cardhost/config/environment';
import { fieldTypeMappings, fieldComponents } from '../utils/mappings';
import drag from '../motions/drag';
import move from 'ember-animated/motions/move';

const { environment } = ENV;

export default class CardManipulator extends Component {
  fieldTypeMappings = fieldTypeMappings;

  @service data;
  @service router;
  @service cardstackSession;
  @service cssModeToggle;
  @service draggable;

  @tracked statusMsg;
  @tracked card;
  @tracked selectedField;
  @tracked isDragging;
  @tracked cardId;
  @tracked cardSelected = true;
  @tracked fieldComponents = fieldComponents;

  constructor(...args) {
    super(...args);

    this.card = this.args.card;
  }

  get cardJson() {
    if (!this.card) {
      return null;
    }
    return JSON.stringify(this.card.json, null, 2);
  }

  get isDirtyStr() {
    return this.card.isDirty.toString();
  }

  get newFieldName() {
    /**
      Returns field name in the form field-12, incrementing from the highest
      existing field number. Ex: if the highest is field-15, this will return
      field-16. If there are no fields, it returns field-1.
    */
    let existingFields = this.card.isolatedFields.map(field => field.name);
    let autogeneratedFieldNames = existingFields.filter(item => /^field-\d+$/.test(item));
    let fieldNumbers = autogeneratedFieldNames.map(item => Number(item.split('-')[1]));
    let newNumber = fieldNumbers.length ? Math.max(...fieldNumbers) + 1 : 1;
    return `field-${newNumber}`;
  }

  get didUpdate() {
    if (this.args.card && !this.args.card.isNew && (!this.card || this.args.card.id !== this.card.id)) {
      this.card = this.args.card;
    }
    return null;
  }

  @action
  updateCard(element, [card]) {
    if (!card.isNew) {
      this.card = card;
    }
  }

  @task(function*() {
    this.statusMsg = null;
    try {
      yield this.card.delete();
    } catch (e) {
      console.error(e); // eslint-disable-line no-console
      this.statusMsg = `card ${this.card.name} was NOT successfully deleted: ${e.message}`;
      return;
    }
    this.router.transitionTo('index');
  })
  deleteCard;

  @action
  removeField(fieldNonce) {
    if (fieldNonce == null || !this.card) {
      return;
    }

    // using field nonce in order to be resiliant to the scenario where the user deletes the name of the field too
    let field = this.card.getFieldByNonce(fieldNonce);

    if (field === this.selectedField) {
      this.cardSelected = true;
    }

    field.remove();
  }

  @action
  addField(displayType, name, isEmbedded, value, position) {
    let type = displayType ? fieldTypeMappings[displayType] : null;
    if (!this.card || !type || !name) {
      return;
    }

    let field = this.card.addField({
      type,
      position,
      name: dasherize(name).toLowerCase(),
      neededWhenEmbedded: isEmbedded,
    });

    if (value != null) {
      field.setValue(value);
    }
  }

  @action
  setPosition(fieldName, position) {
    if (!fieldName || !this.card || position == null) {
      return;
    }

    let card = this.card;
    card.moveField(card.getField(fieldName), position);
  }

  @action
  setNeededWhenEmbedded(fieldName, neededWhenEmbedded, evt) {
    // this prevents 2-way data binding from trying to alter the Field
    // instance's neededWhenEmbedded value, which is bound to the input
    // that fired this action. Our data service API is very unforgiving when
    // you try to change the Field's state outside of the official API
    // (which is what ember is trying to do). Ember gets mad when it sees
    // that it can't alter the Field's state via the 2-way binding and
    // makes lots of noise. interestingly, this issue only seems to happen
    // when running tests. This work around has yucky visual side effects,
    // so only performing in the test env. A better solution would be to use/make
    // a one-way input control for setting the field.neededWhenEmbedded value.
    // The <Input> component is unfortunately, is not a one-way input helper
    if (environment === 'test') {
      evt.preventDefault();
    }

    this.card.getField(fieldName).setNeededWhenEmbedded(neededWhenEmbedded);
  }

  @action
  setFieldValue(fieldName, value) {
    if (!fieldName || !this.card) {
      return;
    }
    this.card.getField(fieldName).setValue(value);
  }

  @action
  setFieldName(oldFieldName, newFieldName) {
    this.card.getField(oldFieldName).setName(newFieldName);
    this.card.getField(newFieldName).setLabel(startCase(newFieldName));
  }

  @action
  setFieldLabel(fieldName, label) {
    this.card.getField(fieldName).setLabel(label);
  }

  @action
  setFieldInstructions(fieldName, instructions) {
    this.card.getField(fieldName).setInstructions(instructions);
  }

  @action
  preview() {
    this.router.transitionTo('cards.card.edit.layout', this.card);
  }

  @action
  delete() {
    this.deleteCard.perform();
  }

  @action
  initDrag() {
    this.isDragging = true;
  }

  @action dropField(position, onFinishDrop, evt) {
    let draggable = this.draggable.getField();

    if (!draggable) {
      return;
    }

    onFinishDrop();

    let field;

    if (draggable.name) {
      let fieldName = draggable.name;
      if (fieldName) {
        field = this.card.getField(fieldName);
        let newPosition = field.position < position ? position - 1 : position;
        this.setPosition(fieldName, newPosition);
      }
    } else {
      field = this.card.addField({
        type: this.fieldTypeMappings[draggable.type],
        position: position,
        name: this.newFieldName,
        neededWhenEmbedded: false,
      });
    }

    this.draggable.clearField();
    this.draggable.clearDropzone();

    if (field) {
      this.selectField(field, evt);
    }
  }

  @action selectField(field, evt) {
    if (field && field.isDestroyed) {
      return;
    }

    // Toggling the selected field in tests is baffling me, using something more brute force
    if (environment === 'test' && this.selectedField === field) {
      return;
    }

    // we have to focus the clicked element to take focus away from the card.
    // to do that we have to give the element tabindex = 0 temporarily.
    // but if the element already has a tabindex (i.e. an input), we need
    // to make sure not to clobber it's original tabindex
    let tabIndex = evt.target.tabIndex;
    if (tabIndex === -1) {
      evt.target.tabIndex = 0;
      evt.target.focus();
      evt.target.blur();
      evt.target.tabIndex = tabIndex;
    } else {
      evt.target.focus();
    }

    this.selectedField = field;
    this.cardSelected = false;
  }

  @action
  selectFieldType(field, event) {
    if (!this.draggable.isDragging) {
      this.beginDragging(field, event);
    } else {
      this.draggable.clearField();
      this.draggable.setDragging(false);
    }
  }

  @action
  beginDragging(field, dragEvent) {
    let dragState;
    let draggableService = this.draggable;

    function stopMouse() {
      field.dragState = null;
      let dropzone = draggableService.getDropzone();
      if (dropzone) {
        draggableService.drop();
      } else {
        // we mouseup somewhere that isn't a dropzone
        draggableService.clearField();
        // we do this so that we can animate the field back to the left edge
        this.fieldComponents = this.fieldComponents.map(obj => obj); // oh glimmer, you so silly...
      }

      window.removeEventListener('mouseup', stopMouse);
      window.removeEventListener('mousemove', updateMouse);

      // remove ghost element from DOM
      let ghostEl = document.getElementById('ghost-element');
      if (ghostEl) {
        ghostEl.remove();
      }

      return false;
    }

    function updateMouse(event) {
      dragState.latestPointerX = event.x;
      dragState.latestPointerY = event.y;

      draggableService.setDragging(true);

      // in order for the drop zone to trigger a mouseenter/mouseleave event
      // we need to temporarily hide the dragged element
      let fieldEl = dragEvent.target.closest('.ch-catalog-field');
      fieldEl.style.visibility = 'hidden';
      let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
      fieldEl.style.visibility = 'visible';

      // this can happen when you drag the mouse outside the viewport
      if (!elemBelow) {
        return;
      }

      let dropzoneBelow = elemBelow.closest('.drop-zone');
      let currentDropzone = draggableService.getDropzone();

      if (currentDropzone !== dropzoneBelow) {
        if (currentDropzone) {
          draggableService.clearDropzone();
        }
        if (dropzoneBelow) {
          draggableService.setDropzone(elemBelow);
        }
      }
    }

    if (dragEvent instanceof KeyboardEvent) {
      // This is a keyboard-controlled "drag" instead of a real mouse
      // drag.
      dragState = {
        usingKeyboard: true,
        xStep: 0,
        yStep: 0,
      };
    } else {
      dragState = {
        usingKeyboard: false,
        initialPointerX: dragEvent.x,
        initialPointerY: dragEvent.y,
        latestPointerX: dragEvent.x,
        latestPointerY: dragEvent.y,
      };
      window.addEventListener('mouseup', stopMouse.bind(this));
      window.addEventListener('mousemove', updateMouse);
    }
    field.dragState = dragState;
    this.draggable.setField(field);
    this.fieldComponents = this.fieldComponents.map(obj => obj); // oh glimmer, you so silly...
  }

  *transition({ keptSprites }) {
    let activeSprite = keptSprites.find(sprite => sprite.owner.value.dragState);
    let others = keptSprites.filter(sprite => sprite !== activeSprite);

    if (activeSprite) {
      drag(activeSprite, {
        others,
      });
      let ghostElement = getGhostFromSprite(activeSprite);
      activeSprite.element.parentElement.appendChild(ghostElement);
      others.forEach(move);
    } else {
      keptSprites.forEach(sprite => {
        move(sprite, { duration: 300 });
      });
    }
  }
}

function getGhostFromSprite(sprite) {
  let ghostElement = sprite.element.cloneNode(true);
  for (let [key, value] of Object.entries(sprite.initialComputedStyle)) {
    ghostElement.style[key] = value;
  }
  let { top, left } = sprite.initialBounds;
  ghostElement.style.position = 'fixed';
  ghostElement.style.top = top;
  ghostElement.style.left = left;
  ghostElement.style.zIndex = '0';
  ghostElement.style.opacity = '0.4';
  ghostElement.id = 'ghost-element';

  return ghostElement;
}
<commit_msg>make sure to remove mouseup listener<commit_after>import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { dasherize } from '@ember/string';
import { startCase } from 'lodash';
import { task } from 'ember-concurrency';
import ENV from '@cardstack/cardhost/config/environment';
import { fieldTypeMappings, fieldComponents } from '../utils/mappings';
import drag from '../motions/drag';
import move from 'ember-animated/motions/move';

const { environment } = ENV;

export default class CardManipulator extends Component {
  fieldTypeMappings = fieldTypeMappings;

  @service data;
  @service router;
  @service cardstackSession;
  @service cssModeToggle;
  @service draggable;

  @tracked statusMsg;
  @tracked card;
  @tracked selectedField;
  @tracked isDragging;
  @tracked cardId;
  @tracked cardSelected = true;
  @tracked fieldComponents = fieldComponents;
  @tracked stopMouse;

  constructor(...args) {
    super(...args);

    this.card = this.args.card;
  }

  get cardJson() {
    if (!this.card) {
      return null;
    }
    return JSON.stringify(this.card.json, null, 2);
  }

  get isDirtyStr() {
    return this.card.isDirty.toString();
  }

  get newFieldName() {
    /**
      Returns field name in the form field-12, incrementing from the highest
      existing field number. Ex: if the highest is field-15, this will return
      field-16. If there are no fields, it returns field-1.
    */
    let existingFields = this.card.isolatedFields.map(field => field.name);
    let autogeneratedFieldNames = existingFields.filter(item => /^field-\d+$/.test(item));
    let fieldNumbers = autogeneratedFieldNames.map(item => Number(item.split('-')[1]));
    let newNumber = fieldNumbers.length ? Math.max(...fieldNumbers) + 1 : 1;
    return `field-${newNumber}`;
  }

  get didUpdate() {
    if (this.args.card && !this.args.card.isNew && (!this.card || this.args.card.id !== this.card.id)) {
      this.card = this.args.card;
    }
    return null;
  }

  @action
  updateCard(element, [card]) {
    if (!card.isNew) {
      this.card = card;
    }
  }

  @task(function*() {
    this.statusMsg = null;
    try {
      yield this.card.delete();
    } catch (e) {
      console.error(e); // eslint-disable-line no-console
      this.statusMsg = `card ${this.card.name} was NOT successfully deleted: ${e.message}`;
      return;
    }
    this.router.transitionTo('index');
  })
  deleteCard;

  @action
  removeField(fieldNonce) {
    if (fieldNonce == null || !this.card) {
      return;
    }

    // using field nonce in order to be resiliant to the scenario where the user deletes the name of the field too
    let field = this.card.getFieldByNonce(fieldNonce);

    if (field === this.selectedField) {
      this.cardSelected = true;
    }

    field.remove();
  }

  @action
  addField(displayType, name, isEmbedded, value, position) {
    let type = displayType ? fieldTypeMappings[displayType] : null;
    if (!this.card || !type || !name) {
      return;
    }

    let field = this.card.addField({
      type,
      position,
      name: dasherize(name).toLowerCase(),
      neededWhenEmbedded: isEmbedded,
    });

    if (value != null) {
      field.setValue(value);
    }
  }

  @action
  setPosition(fieldName, position) {
    if (!fieldName || !this.card || position == null) {
      return;
    }

    let card = this.card;
    card.moveField(card.getField(fieldName), position);
  }

  @action
  setNeededWhenEmbedded(fieldName, neededWhenEmbedded, evt) {
    // this prevents 2-way data binding from trying to alter the Field
    // instance's neededWhenEmbedded value, which is bound to the input
    // that fired this action. Our data service API is very unforgiving when
    // you try to change the Field's state outside of the official API
    // (which is what ember is trying to do). Ember gets mad when it sees
    // that it can't alter the Field's state via the 2-way binding and
    // makes lots of noise. interestingly, this issue only seems to happen
    // when running tests. This work around has yucky visual side effects,
    // so only performing in the test env. A better solution would be to use/make
    // a one-way input control for setting the field.neededWhenEmbedded value.
    // The <Input> component is unfortunately, is not a one-way input helper
    if (environment === 'test') {
      evt.preventDefault();
    }

    this.card.getField(fieldName).setNeededWhenEmbedded(neededWhenEmbedded);
  }

  @action
  setFieldValue(fieldName, value) {
    if (!fieldName || !this.card) {
      return;
    }
    this.card.getField(fieldName).setValue(value);
  }

  @action
  setFieldName(oldFieldName, newFieldName) {
    this.card.getField(oldFieldName).setName(newFieldName);
    this.card.getField(newFieldName).setLabel(startCase(newFieldName));
  }

  @action
  setFieldLabel(fieldName, label) {
    this.card.getField(fieldName).setLabel(label);
  }

  @action
  setFieldInstructions(fieldName, instructions) {
    this.card.getField(fieldName).setInstructions(instructions);
  }

  @action
  preview() {
    this.router.transitionTo('cards.card.edit.layout', this.card);
  }

  @action
  delete() {
    this.deleteCard.perform();
  }

  @action
  initDrag() {
    this.isDragging = true;
  }

  @action dropField(position, onFinishDrop, evt) {
    let draggable = this.draggable.getField();

    if (!draggable) {
      return;
    }

    onFinishDrop();

    let field;

    if (draggable.name) {
      let fieldName = draggable.name;
      if (fieldName) {
        field = this.card.getField(fieldName);
        let newPosition = field.position < position ? position - 1 : position;
        this.setPosition(fieldName, newPosition);
      }
    } else {
      field = this.card.addField({
        type: this.fieldTypeMappings[draggable.type],
        position: position,
        name: this.newFieldName,
        neededWhenEmbedded: false,
      });
    }

    this.draggable.clearField();
    this.draggable.clearDropzone();

    if (field) {
      this.selectField(field, evt);
    }
  }

  @action selectField(field, evt) {
    if (field && field.isDestroyed) {
      return;
    }

    // Toggling the selected field in tests is baffling me, using something more brute force
    if (environment === 'test' && this.selectedField === field) {
      return;
    }

    // we have to focus the clicked element to take focus away from the card.
    // to do that we have to give the element tabindex = 0 temporarily.
    // but if the element already has a tabindex (i.e. an input), we need
    // to make sure not to clobber it's original tabindex
    let tabIndex = evt.target.tabIndex;
    if (tabIndex === -1) {
      evt.target.tabIndex = 0;
      evt.target.focus();
      evt.target.blur();
      evt.target.tabIndex = tabIndex;
    } else {
      evt.target.focus();
    }

    this.selectedField = field;
    this.cardSelected = false;
  }

  @action
  selectFieldType(field, event) {
    if (!this.draggable.isDragging) {
      this.beginDragging(field, event);
    } else {
      this.draggable.clearField();
      this.draggable.setDragging(false);
    }
  }

  @action
  beginDragging(field, dragEvent) {
    let dragState;
    let draggableService = this.draggable;

    function stopMouse() {
      field.dragState = null;
      let dropzone = draggableService.getDropzone();
      if (dropzone) {
        draggableService.drop();
      } else {
        // we mouseup somewhere that isn't a dropzone
        draggableService.clearField();
        // we do this so that we can animate the field back to the left edge
        this.fieldComponents = this.fieldComponents.map(obj => obj); // oh glimmer, you so silly...
      }

      window.removeEventListener('mouseup', this.stopMouse);
      window.removeEventListener('mousemove', updateMouse);

      // remove ghost element from DOM
      let ghostEl = document.getElementById('ghost-element');
      if (ghostEl) {
        ghostEl.remove();
      }

      return false;
    }

    function updateMouse(event) {
      dragState.latestPointerX = event.x;
      dragState.latestPointerY = event.y;

      draggableService.setDragging(true);

      // in order for the drop zone to trigger a mouseenter/mouseleave event
      // we need to temporarily hide the dragged element
      let fieldEl = dragEvent.target.closest('.ch-catalog-field');
      fieldEl.style.visibility = 'hidden';
      let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
      fieldEl.style.visibility = 'visible';

      // this can happen when you drag the mouse outside the viewport
      if (!elemBelow) {
        return;
      }

      let dropzoneBelow = elemBelow.closest('.drop-zone');
      let currentDropzone = draggableService.getDropzone();

      if (currentDropzone !== dropzoneBelow) {
        if (currentDropzone) {
          draggableService.clearDropzone();
        }
        if (dropzoneBelow) {
          draggableService.setDropzone(elemBelow);
        }
      }
    }

    if (dragEvent instanceof KeyboardEvent) {
      // This is a keyboard-controlled "drag" instead of a real mouse
      // drag.
      dragState = {
        usingKeyboard: true,
        xStep: 0,
        yStep: 0,
      };
    } else {
      dragState = {
        usingKeyboard: false,
        initialPointerX: dragEvent.x,
        initialPointerY: dragEvent.y,
        latestPointerX: dragEvent.x,
        latestPointerY: dragEvent.y,
      };
      this.stopMouse = stopMouse.bind(this);
      window.addEventListener('mouseup', this.stopMouse);
      window.addEventListener('mousemove', updateMouse);
    }
    field.dragState = dragState;
    this.draggable.setField(field);
    this.fieldComponents = this.fieldComponents.map(obj => obj); // oh glimmer, you so silly...
  }

  *transition({ keptSprites }) {
    let activeSprite = keptSprites.find(sprite => sprite.owner.value.dragState);
    let others = keptSprites.filter(sprite => sprite !== activeSprite);

    if (activeSprite) {
      drag(activeSprite, {
        others,
      });
      let ghostElement = getGhostFromSprite(activeSprite);
      activeSprite.element.parentElement.appendChild(ghostElement);
      others.forEach(move);
    } else {
      keptSprites.forEach(sprite => {
        move(sprite, { duration: 300 });
      });
    }
  }
}

function getGhostFromSprite(sprite) {
  let ghostElement = sprite.element.cloneNode(true);
  for (let [key, value] of Object.entries(sprite.initialComputedStyle)) {
    ghostElement.style[key] = value;
  }
  let { top, left } = sprite.initialBounds;
  ghostElement.style.position = 'fixed';
  ghostElement.style.top = top;
  ghostElement.style.left = left;
  ghostElement.style.zIndex = '0';
  ghostElement.style.opacity = '0.4';
  ghostElement.id = 'ghost-element';

  return ghostElement;
}
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const ReactRelayCompatContainerBuilder = require('./ReactRelayCompatContainerBuilder');
const ReactRelayQueryRenderer = require('../modern/ReactRelayQueryRenderer');
const RelayCompatContainer = require('./react/RelayCompatContainer');
const RelayCompatMutations = require('./mutations/RelayCompatMutations');
const RelayCompatPaginationContainer = require('./react/RelayCompatPaginationContainer');
const RelayCompatRefetchContainer = require('./react/RelayCompatRefetchContainer');

const {graphql, fetchQuery} = require('RelayRuntime');

export type {
  Disposable,
} from '../classic/environment/RelayCombinedEnvironmentTypes';
export type {DataID} from '../classic/tools/RelayInternalTypes';
export type {Variables} from '../classic/tools/RelayTypes';
export type {
  RelayPaginationProp,
  RelayProp,
  RelayRefetchProp,
} from '../modern/ReactRelayTypes';
export type {
  GraphQLTaggedNode,
  IEnvironment,
  OperationSelector,
  RelayContext,
  Selector,
  Snapshot,
} from 'RelayRuntime';

/**
 * The public interface to React Relay which supports a compatibility mode to
 * continue to work with the classic React runtime.
 */
module.exports = {
  QueryRenderer: ReactRelayQueryRenderer,
  applyOptimisticMutation: RelayCompatMutations.applyUpdate,
  commitMutation: RelayCompatMutations.commitUpdate,
  createFragmentContainer: (RelayCompatContainer.createContainer: $FlowFixMe),
  createPaginationContainer: (RelayCompatPaginationContainer.createContainer: $FlowFixMe),
  createRefetchContainer: (RelayCompatRefetchContainer.createContainer: $FlowFixMe),
  fetchQuery: fetchQuery,
  graphql: graphql,
  injectDefaultVariablesProvider:
    ReactRelayCompatContainerBuilder.injectDefaultVariablesProvider,
};
<commit_msg>Flow type RelayCompathg<commit_after>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const ReactRelayCompatContainerBuilder = require('./ReactRelayCompatContainerBuilder');
const ReactRelayQueryRenderer = require('../modern/ReactRelayQueryRenderer');
const RelayCompatContainer = require('./react/RelayCompatContainer');
const RelayCompatMutations = require('./mutations/RelayCompatMutations');
const RelayCompatPaginationContainer = require('./react/RelayCompatPaginationContainer');
const RelayCompatRefetchContainer = require('./react/RelayCompatRefetchContainer');

const {graphql, fetchQuery} = require('RelayRuntime');

export type {
  Disposable,
} from '../classic/environment/RelayCombinedEnvironmentTypes';
export type {DataID} from '../classic/tools/RelayInternalTypes';
export type {Variables} from '../classic/tools/RelayTypes';
export type {
  RelayPaginationProp,
  RelayProp,
  RelayRefetchProp,
} from '../modern/ReactRelayTypes';
export type {
  GraphQLTaggedNode,
  IEnvironment,
  OperationSelector,
  RelayContext,
  Selector,
  Snapshot,
} from 'RelayRuntime';

/**
 * The public interface to React Relay which supports a compatibility mode to
 * continue to work with the classic Relay runtime.
 */
module.exports = {
  QueryRenderer: ReactRelayQueryRenderer,

  applyOptimisticMutation: RelayCompatMutations.applyUpdate,
  commitMutation: RelayCompatMutations.commitUpdate,
  createFragmentContainer: RelayCompatContainer.createContainer,
  createPaginationContainer: RelayCompatPaginationContainer.createContainer,
  createRefetchContainer: RelayCompatRefetchContainer.createContainer,
  fetchQuery: fetchQuery,
  graphql: graphql,

  injectDefaultVariablesProvider:
    ReactRelayCompatContainerBuilder.injectDefaultVariablesProvider,
};
<|endoftext|>