<commit_before>/**
 * @file DraggableTreeNode component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Droppable, Draggable} from 'react-beautiful-dnd';
import classNames from 'classnames';

import CircularLoading from '../CircularLoading';
import TipProvider from '../TipProvider';
import IconButton from '../IconButton';
import Radio from '../Radio';
import Checkbox from '../Checkbox';

import Theme from '../Theme';
import Position from '../_statics/Position';
import SelectMode from '../_statics/SelectMode';
import VirtualRoot from '../_statics/VirtualRoot';

import Util from '../_vendors/Util';
import Calculation from '../_vendors/Calculation';

class DraggableTreeNode extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            collapsed: props.data && props.data[VirtualRoot] ? false : props.collapsed
        };

    }

    toggleTreeNode = e => {

        e && e.stopPropagation();

        const {onNodeToggleStart} = this.props;
        onNodeToggleStart && onNodeToggleStart();

        this.setState({
            collapsed: !this.state.collapsed
        }, () => {
            const {onNodeToggleEnd} = this.props;
            onNodeToggleEnd && onNodeToggleEnd();
        });

    };

    handleCheckboxChange = e => {

        const {data, path, value, onSelect, onDeselect} = this.props;

        if (!Calculation.isItemChecked(data, value, this.props)) {
            onSelect && onSelect(data, path, e);
        } else {
            onDeselect && onDeselect(data, path, e);
        }

    };

    handleRadioChange = e => {
        const {data, path, onSelect} = this.props;
        onSelect && onSelect(data, path, e);
    };

    handleClick = e => {

        e.preventDefault();

        const {data, path, disabled, isLoading, readOnly} = this.props;

        if (disabled || isLoading || readOnly || data.disabled || data.isLoading || data.readOnly) {
            return;
        }

        const {onClick} = this.props;
        onClick && onClick(data, path, e);

        const {selectMode} = this.props;

        switch (selectMode) {
            case SelectMode.MULTI_SELECT:
                this.handleCheckboxChange(e);
                return;
            case SelectMode.SINGLE_SELECT:
                this.handleRadioChange(e);
                return;
        }

    };

    /* eslint-disable complexity */
    render() {

        const {

                index, depth, path, theme, selectTheme, selectMode, data, value,
                disabled, isLoading, readOnly, allowCollapse, isSelectRecursive,
                valueField, displayField, descriptionField,

                collapsedIconCls, expandedIconCls, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                renderer, onMouseEnter, onMouseLeave,

                isDragging, isNodeToggling

            } = this.props,
            {collapsed} = this.state,

            checked = Calculation.isItemChecked(data, value, this.props),
            isNodeLoading = data.isLoading || isLoading,
            isNodeDisabled = data.disabled || disabled || isNodeLoading,
            isVirtual = VirtualRoot in data,
            loadingIconPosition = (data.rightIconCls && !data.iconCls) ? 'right' : 'left';

        return (
            <Droppable droppableId={'' + data.id}
                       type={data.id}
                       key={data.id}>
                {
                    dropProvided => (

                            {
                                isVirtual ?
                                    null
                                    :
                                    <TipProvider tipContent={data.tip}
                                                 position={data.tipPosition}>

                                        <div className={classNames('draggable-tree-node', {
                                            [`theme-${theme}`]: theme,
                                            dragging: isDragging,
                                            [data.className]: data.className
                                        })}
                                             style={{
                                                 ...data.style,
                                                 paddingLeft: (depth + 1) * 20
                                             }}
                                             disabled={isNodeDisabled}
                                             readOnly={readOnly}
                                             onClick={this.handleClick}
                                             onMouseEnter={onMouseEnter}
                                             onMouseLeave={onMouseLeave}>

                                                {
                                                    allowCollapse && data.children && data.children.length > 0 ?
                                                        <IconButton className="draggable-tree-node-collapse-icon"
                                                                    iconCls={collapsed ?
                                                                        data.collapsedIconCls || collapsedIconCls
                                                                        :
                                                                        data.expandedIconCls || expandedIconCls}
                                                                    onClick={this.toggleTreeNode}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    selectMode === SelectMode.SINGLE_SELECT && (radioUncheckedIconCls || radioCheckedIconCls) ?
                                                        <Radio className="draggable-tree-node-select"
                                                               theme={selectTheme}
                                                               checked={checked}
                                                               disabled={isNodeDisabled}
                                                               uncheckedIconCls={data.radioUncheckedIconCls || radioUncheckedIconCls}
                                                               checkedIconCls={data.radioCheckedIconCls || radioCheckedIconCls}
                                                               disableTouchRipple={true}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    selectMode === SelectMode.MULTI_SELECT ?
                                                        <Checkbox className="draggable-tree-node-select"
                                                                  theme={selectTheme}
                                                                  checked={checked}
                                                                  indeterminate={isSelectRecursive ? indeterminate : false}
                                                                  disabled={isNodeDisabled}
                                                                  uncheckedIconCls={data.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                                                  checkedIconCls={data.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                                                  indeterminateIconCls={data.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                                                  disableTouchRipple={true}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    isNodeLoading && loadingIconPosition === 'left' ?

                                                            <CircularLoading className="button-loading-icon"
                                                                             size="small"/>

                                                        :
                                                        (
                                                            data.iconCls ?
                                                                <i className={`button-icon button-icon-left ${data.iconCls}`}
                                                                   aria-hidden="true">
                                                                :
                                                                null
                                                        )
                                                }

                                                {
                                                    data.itemRenderer && typeof data.itemRenderer === 'function' ?
                                                        data.itemRenderer(data, index)
                                                        :
                                                        (
                                                            renderer && typeof renderer === 'function' ?
                                                                renderer(data, index)
                                                                :
                                                                (
                                                                    data[descriptionField] ?

                                                                                {Util.getTextByDisplayField(data, displayField, valueField)}

                                                                                {data[descriptionField]}

                                                                        :
                                                                        Util.getTextByDisplayField(data, displayField, valueField)
                                                                )
                                                        )
                                                }

                                                {
                                                    isNodeLoading && loadingIconPosition === 'right' ?
                                                        <CircularLoading
                                                            className="button-icon button-icon-right button-loading-icon"
                                                            size="small"/>
                                                        :
                                                        (
                                                            data.rightIconCls ?
                                                                <i className={`button-icon button-icon-right ${data.rightIconCls}`}
                                                                   aria-hidden="true">
                                                                :
                                                                null
                                                        )
                                                }

                                    </TipProvider>
                            }

                            {
                                data.children && data.children.length > 0 ?

                                        {
                                            data.children.map((item, index) => {

                                                const path = path ?
                                                    [...path, {index, node: item}]
                                                    :
                                                    [{index, node: item}];

                                                return (
                                                    <Draggable key={item.id}
                                                               draggableId={'' + item.id}
                                                               type={data.id}
                                                               disableInteractiveElementBlocking={false}
                                                               isDragDisabled={isNodeToggling || isNodeDisabled}
                                                               index={index}>
                                                        {
                                                            (dragProvided, dragSnapshot) => (

                                                                        <DraggableTreeNode {...this.props}
                                                                                key={index}
                                                                                data={item}
                                                                                index={index}
                                                                                depth={depth + (isVirtual ? 0 : 1)}
                                                                                path={path}
                                                                                isDragging={dragSnapshot.isDragging}/>

                                                            )
                                                        }
                                                    </Draggable>
                                                );
                                            })
                                        }

                                    :
                                    null
                            }

                            {dropProvided.placeholder}

                    )
                }

            </Droppable>
        );

    }
}

DraggableTreeNode.propTypes = {

    index: PropTypes.number,
    depth: PropTypes.number,
    path: PropTypes.array,

    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    data: PropTypes.object,
    value: PropTypes.any,

    idField: PropTypes.string,
    valueField: PropTypes.string,
    displayField: PropTypes.string,
    descriptionField: PropTypes.string,

    disabled: PropTypes.bool,
    isLoading: PropTypes.bool,
    readOnly: PropTypes.bool,
    allowCollapse: PropTypes.bool,
    collapsed: PropTypes.bool,
    isNodeToggling: PropTypes.bool,
    isSelectRecursive: PropTypes.bool,

    renderer: PropTypes.func,

    collapsedIconCls: PropTypes.string,
    expandedIconCls: PropTypes.string,
    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    onClick: PropTypes.func,
    onSelect: PropTypes.func,
    onDeselect: PropTypes.func,
    onMouseEnter: PropTypes.func,
    onMouseLeave: PropTypes.func,
    onNodeToggleStart: PropTypes.func,
    onNodeToggleEnd: PropTypes.func,

    isDragging: PropTypes.bool

};

DraggableTreeNode.defaultProps = {

    index: 0,
    depth: 0,

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    disabled: false,
    isLoading: false,
    readOnly: false,
    allowCollapse: true,
    collapsed: false,
    isNodeToggling: false,
    isSelectRecursive: false,

    tipPosition: Position.BOTTOM,

    collapsedIconCls: 'fas fa-caret-right',
    expandedIconCls: 'fas fa-caret-down',
    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square',

    isDragging: false

};

export default DraggableTreeNode;
<commit_msg>update DraggableTreeNode<commit_after>/**
 * @file DraggableTreeNode component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Droppable, Draggable} from 'react-beautiful-dnd';
import classNames from 'classnames';

import CircularLoading from '../CircularLoading';
import TipProvider from '../TipProvider';
import IconButton from '../IconButton';
import Radio from '../Radio';
import Checkbox from '../Checkbox';

import Theme from '../Theme';
import Position from '../_statics/Position';
import SelectMode from '../_statics/SelectMode';
import VirtualRoot from '../_statics/VirtualRoot';

import Util from '../_vendors/Util';
import Calculation from '../_vendors/Calculation';

class DraggableTreeNode extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            collapsed: this.isCollapsed(props)
        };

    }

    isCollapsed = (props = this.props) => {

        const {data, isNodeCollapsed, collapsed, index, depth, path} = props;

        if (data && data[VirtualRoot]) {
            return false;
        }

        if (isNodeCollapsed) {
            if (typeof isNodeCollapsed === 'function') {
                return isNodeCollapsed(data, index, depth, path);
            } else {
                return !!isNodeCollapsed;
            }
        }

        return collapsed;

    };

    toggleTreeNode = e => {

        e && e.stopPropagation();

        const {onNodeToggleStart} = this.props;
        onNodeToggleStart && onNodeToggleStart();

        this.setState({
            collapsed: !this.state.collapsed
        }, () => {
            const {onNodeToggleEnd} = this.props;
            onNodeToggleEnd && onNodeToggleEnd();
        });

    };

    handleCheckboxChange = e => {

        const {data, path, value, onSelect, onDeselect} = this.props;

        if (!Calculation.isItemChecked(data, value, this.props)) {
            onSelect && onSelect(data, path, e);
        } else {
            onDeselect && onDeselect(data, path, e);
        }

    };

    handleRadioChange = e => {
        const {data, path, onSelect} = this.props;
        onSelect && onSelect(data, path, e);
    };

    handleClick = e => {

        e.preventDefault();

        const {data, path, disabled, isLoading, readOnly} = this.props;

        if (disabled || isLoading || readOnly || data.disabled || data.isLoading || data.readOnly) {
            return;
        }

        const {onClick} = this.props;
        onClick && onClick(data, path, e);

        const {selectMode} = this.props;

        switch (selectMode) {
            case SelectMode.MULTI_SELECT:
                this.handleCheckboxChange(e);
                return;
            case SelectMode.SINGLE_SELECT:
                this.handleRadioChange(e);
                return;
        }

    };

    /* eslint-disable complexity */
    render() {

        const {

                index, depth, path, theme, selectTheme, selectMode, data, value,
                disabled, isLoading, readOnly, allowCollapse, isSelectRecursive,
                valueField, displayField, descriptionField,

                collapsedIconCls, expandedIconCls, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                renderer, onMouseEnter, onMouseLeave,

                isDragging, isNodeToggling

            } = this.props,
            {collapsed} = this.state,

            checked = Calculation.isItemChecked(data, value, this.props),
            isNodeLoading = data.isLoading || isLoading,
            isNodeDisabled = data.disabled || disabled || isNodeLoading,
            isVirtual = VirtualRoot in data,
            loadingIconPosition = (data.rightIconCls && !data.iconCls) ? 'right' : 'left';

        return (
            <Droppable droppableId={'' + data.id}
                       type={data.id}
                       key={data.id}>
                {
                    dropProvided => (

                            {
                                isVirtual ?
                                    null
                                    :
                                    <TipProvider tipContent={data.tip}
                                                 position={data.tipPosition}>

                                        <div className={classNames('draggable-tree-node', {
                                            [`theme-${theme}`]: theme,
                                            dragging: isDragging,
                                            [data.className]: data.className
                                        })}
                                             style={{
                                                 ...data.style,
                                                 paddingLeft: (depth + 1) * 20
                                             }}
                                             disabled={isNodeDisabled}
                                             readOnly={readOnly}
                                             onClick={this.handleClick}
                                             onMouseEnter={onMouseEnter}
                                             onMouseLeave={onMouseLeave}>

                                                {
                                                    allowCollapse && data.children && data.children.length > 0 ?
                                                        <IconButton className="draggable-tree-node-collapse-icon"
                                                                    iconCls={collapsed ?
                                                                        data.collapsedIconCls || collapsedIconCls
                                                                        :
                                                                        data.expandedIconCls || expandedIconCls}
                                                                    onClick={this.toggleTreeNode}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    selectMode === SelectMode.SINGLE_SELECT && (radioUncheckedIconCls || radioCheckedIconCls) ?
                                                        <Radio className="draggable-tree-node-select"
                                                               theme={selectTheme}
                                                               checked={checked}
                                                               disabled={isNodeDisabled}
                                                               uncheckedIconCls={data.radioUncheckedIconCls || radioUncheckedIconCls}
                                                               checkedIconCls={data.radioCheckedIconCls || radioCheckedIconCls}
                                                               disableTouchRipple={true}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    selectMode === SelectMode.MULTI_SELECT ?
                                                        <Checkbox className="draggable-tree-node-select"
                                                                  theme={selectTheme}
                                                                  checked={checked}
                                                                  indeterminate={isSelectRecursive ? indeterminate : false}
                                                                  disabled={isNodeDisabled}
                                                                  uncheckedIconCls={data.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                                                                  checkedIconCls={data.checkboxCheckedIconCls || checkboxCheckedIconCls}
                                                                  indeterminateIconCls={data.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                                                                  disableTouchRipple={true}/>
                                                        :
                                                        null
                                                }

                                                {
                                                    isNodeLoading && loadingIconPosition === 'left' ?

                                                            <CircularLoading className="button-loading-icon"
                                                                             size="small"/>

                                                        :
                                                        (
                                                            data.iconCls ?
                                                                <i className={`button-icon button-icon-left ${data.iconCls}`}
                                                                   aria-hidden="true">
                                                                :
                                                                null
                                                        )
                                                }

                                                {
                                                    data.itemRenderer && typeof data.itemRenderer === 'function' ?
                                                        data.itemRenderer(data, index)
                                                        :
                                                        (
                                                            renderer && typeof renderer === 'function' ?
                                                                renderer(data, index)
                                                                :
                                                                (
                                                                    data[descriptionField] ?

                                                                                {Util.getTextByDisplayField(data, displayField, valueField)}

                                                                                {data[descriptionField]}

                                                                        :
                                                                        Util.getTextByDisplayField(data, displayField, valueField)
                                                                )
                                                        )
                                                }

                                                {
                                                    isNodeLoading && loadingIconPosition === 'right' ?
                                                        <CircularLoading
                                                            className="button-icon button-icon-right button-loading-icon"
                                                            size="small"/>
                                                        :
                                                        (
                                                            data.rightIconCls ?
                                                                <i className={`button-icon button-icon-right ${data.rightIconCls}`}
                                                                   aria-hidden="true">
                                                                :
                                                                null
                                                        )
                                                }

                                    </TipProvider>
                            }

                            {
                                data.children && data.children.length > 0 ?

                                        {
                                            data.children.map((item, index) => {

                                                const path = path ?
                                                    [...path, {index, node: item}]
                                                    :
                                                    [{index, node: item}];

                                                return (
                                                    <Draggable key={item.id}
                                                               draggableId={'' + item.id}
                                                               type={data.id}
                                                               disableInteractiveElementBlocking={false}
                                                               isDragDisabled={isNodeToggling || isNodeDisabled}
                                                               index={index}>
                                                        {
                                                            (dragProvided, dragSnapshot) => (

                                                                        <DraggableTreeNode {...this.props}
                                                                                key={index}
                                                                                data={item}
                                                                                index={index}
                                                                                depth={depth + (isVirtual ? 0 : 1)}
                                                                                path={path}
                                                                                isDragging={dragSnapshot.isDragging}/>

                                                            )
                                                        }
                                                    </Draggable>
                                                );
                                            })
                                        }

                                    :
                                    null
                            }

                            {dropProvided.placeholder}

                    )
                }

            </Droppable>
        );

    }
}

DraggableTreeNode.propTypes = {

    index: PropTypes.number,
    depth: PropTypes.number,
    path: PropTypes.array,

    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    data: PropTypes.object,
    value: PropTypes.any,

    idField: PropTypes.string,
    valueField: PropTypes.string,
    displayField: PropTypes.string,
    descriptionField: PropTypes.string,

    disabled: PropTypes.bool,
    isLoading: PropTypes.bool,
    readOnly: PropTypes.bool,
    allowCollapse: PropTypes.bool,
    collapsed: PropTypes.bool,
    isNodeToggling: PropTypes.bool,
    isSelectRecursive: PropTypes.bool,

    renderer: PropTypes.func,

    collapsedIconCls: PropTypes.string,
    expandedIconCls: PropTypes.string,
    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    onClick: PropTypes.func,
    onSelect: PropTypes.func,
    onDeselect: PropTypes.func,
    onMouseEnter: PropTypes.func,
    onMouseLeave: PropTypes.func,
    onNodeToggleStart: PropTypes.func,
    onNodeToggleEnd: PropTypes.func,

    isDragging: PropTypes.bool

};

DraggableTreeNode.defaultProps = {

    index: 0,
    depth: 0,

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    disabled: false,
    isLoading: false,
    readOnly: false,
    allowCollapse: true,
    collapsed: false,
    isNodeToggling: false,
    isSelectRecursive: false,

    tipPosition: Position.BOTTOM,

    collapsedIconCls: 'fas fa-caret-right',
    expandedIconCls: 'fas fa-caret-down',
    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square',

    isDragging: false

};

export default DraggableTreeNode;
<|endoftext|>


<commit_before>/*
Copyright 2018 New Vector Ltd

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.
*/

class DecryptionFailure {
    constructor(failedEventId) {
        this.failedEventId = failedEventId;
        this.ts = Date.now();
    }
}

function eventIdHash(eventId) {
    return crypto.subtle.digest('SHA-256', eventId);
}

export default class DecryptionFailureTracker {
    // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
    // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
    // are added to `failuresToTrack`.
    failures = [];

    // Every TRACK_INTERVAL_MS (so as to spread the number of hits done on Analytics),
    // one DecryptionFailure of this FIFO is removed and tracked.
    failuresToTrack = [];

    // Event IDs of failures that were tracked previously
    trackedEventHashMap = {
        // [eventIdHash(eventId)]: true
    };

    // Spread the load on `Analytics` by sending at most 1 event per
    // `TRACK_INTERVAL_MS`.
    static TRACK_INTERVAL_MS = 1000;

    // Call `checkFailures` every `CHECK_INTERVAL_MS`.
    static CHECK_INTERVAL_MS = 5000;

    // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before moving
    // the failure to `failuresToTrack`.
    static GRACE_PERIOD_MS = 5000;

    constructor(fn) {
        if (!fn || typeof fn !== 'function') {
            throw new Error('DecryptionFailureTracker requires tracking function');
        }

        this.trackDecryptionFailure = fn;
    }

    loadTrackedEventHashMap() {
        this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes'));
    }

    saveTrackedEventHashMap() {
        localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
    }

    eventDecrypted(e) {
        if (e.isDecryptionFailure()) {
            this.addDecryptionFailureForEvent(e);
        } else {
            // Could be an event in the failures, remove it
            this.removeDecryptionFailuresForEvent(e);
        }
    }

    addDecryptionFailureForEvent(e) {
        this.failures.push(new DecryptionFailure(e.getId()));
    }

    removeDecryptionFailuresForEvent(e) {
        this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
    }

    /**
     * Start checking for and tracking failures.
     * @return {function} a function that clears state and causes DFT to stop checking for
     * and tracking failures.
     */
    start() {
        const checkInterval = setInterval(
            () => this.checkFailures(Date.now()),
            DecryptionFailureTracker.CHECK_INTERVAL_MS,
        );

        const trackInterval = setInterval(
            () => this.trackFailure(),
            DecryptionFailureTracker.TRACK_INTERVAL_MS,
        );

        return () => {
            clearInterval(checkInterval);
            clearInterval(trackInterval);

            this.failures = [];
            this.failuresToTrack = [];
        };
    }

    /**
     * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
     * tracked. Only mark one failure per event ID.
     * @param {number} nowTs the timestamp that represents the time now.
     */
    checkFailures(nowTs) {
        const failuresGivenGrace = [];
        const failuresNotReady = [];
        while (this.failures.length > 0) {
            const f = this.failures.shift();
            if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
                failuresGivenGrace.push(f);
            } else {
                failuresNotReady.push(f);
            }
        }
        this.failures = failuresNotReady;

        // Only track one failure per event
        const dedupedFailuresMap = failuresGivenGrace.reduce(
            (result, failure) => {
                if (!this.trackedEventHashMap[eventIdHash(failure.failedEventId)]) {
                    return {...result, [failure.failedEventId]: failure};
                } else {
                    return result;
                }
            },
            {},
        );

        const trackedEventIds = Object.keys(dedupedFailuresMap);

        this.trackedEventHashMap = trackedEventIds.reduce(
            (result, eventId) => ({...result, [eventIdHash(eventId)]: true}),
            this.trackedEventHashMap,
        );

        this.saveTrackedEventHashMap();

        const dedupedFailures = trackedEventIds.map((k) => dedupedFailuresMap[k]);

        this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures];
    }

    /**
     * If there is a failure that should be tracked, call the given trackDecryptionFailure
     * function with the first failure in the FIFO of failures that should be tracked.
     */
    trackFailure() {
        if (this.failuresToTrack.length > 0) {
            this.trackDecryptionFailure(this.failuresToTrack.shift());
        }
    }
}
<commit_msg>Use a Map instead of Object to preserve failure ordering<commit_after>/*
Copyright 2018 New Vector Ltd

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.
*/

class DecryptionFailure {
    constructor(failedEventId) {
        this.failedEventId = failedEventId;
        this.ts = Date.now();
    }
}

function eventIdHash(eventId) {
    return crypto.subtle.digest('SHA-256', eventId);
}

export default class DecryptionFailureTracker {
    // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
    // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
    // are added to `failuresToTrack`.
    failures = [];

    // Every TRACK_INTERVAL_MS (so as to spread the number of hits done on Analytics),
    // one DecryptionFailure of this FIFO is removed and tracked.
    failuresToTrack = [];

    // Event IDs of failures that were tracked previously
    trackedEventHashMap = {
        // [eventIdHash(eventId)]: true
    };

    // Spread the load on `Analytics` by sending at most 1 event per
    // `TRACK_INTERVAL_MS`.
    static TRACK_INTERVAL_MS = 1000;

    // Call `checkFailures` every `CHECK_INTERVAL_MS`.
    static CHECK_INTERVAL_MS = 5000;

    // Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before moving
    // the failure to `failuresToTrack`.
    static GRACE_PERIOD_MS = 5000;

    constructor(fn) {
        if (!fn || typeof fn !== 'function') {
            throw new Error('DecryptionFailureTracker requires tracking function');
        }

        this.trackDecryptionFailure = fn;
    }

    loadTrackedEventHashMap() {
        this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes'));
    }

    saveTrackedEventHashMap() {
        localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
    }

    eventDecrypted(e) {
        if (e.isDecryptionFailure()) {
            this.addDecryptionFailureForEvent(e);
        } else {
            // Could be an event in the failures, remove it
            this.removeDecryptionFailuresForEvent(e);
        }
    }

    addDecryptionFailureForEvent(e) {
        this.failures.push(new DecryptionFailure(e.getId()));
    }

    removeDecryptionFailuresForEvent(e) {
        this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
    }

    /**
     * Start checking for and tracking failures.
     * @return {function} a function that clears state and causes DFT to stop checking for
     * and tracking failures.
     */
    start() {
        const checkInterval = setInterval(
            () => this.checkFailures(Date.now()),
            DecryptionFailureTracker.CHECK_INTERVAL_MS,
        );

        const trackInterval = setInterval(
            () => this.trackFailure(),
            DecryptionFailureTracker.TRACK_INTERVAL_MS,
        );

        return () => {
            clearInterval(checkInterval);
            clearInterval(trackInterval);

            this.failures = [];
            this.failuresToTrack = [];
        };
    }

    /**
     * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
     * tracked. Only mark one failure per event ID.
     * @param {number} nowTs the timestamp that represents the time now.
     */
    checkFailures(nowTs) {
        const failuresGivenGrace = [];
        const failuresNotReady = [];
        while (this.failures.length > 0) {
            const f = this.failures.shift();
            if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
                failuresGivenGrace.push(f);
            } else {
                failuresNotReady.push(f);
            }
        }
        this.failures = failuresNotReady;

        // Only track one failure per event
        const dedupedFailuresMap = failuresGivenGrace.reduce(
            (map, failure) => {
                if (!this.trackedEventHashMap[eventIdHash(failure.failedEventId)]) {
                    return map.set(failure.failedEventId, failure);
                } else {
                    return map;
                }
            },
            // Use a map to preseve key ordering
            new Map(),
        );

        const trackedEventIds = [...dedupedFailuresMap.keys()];

        this.trackedEventHashMap = trackedEventIds.reduce(
            (result, eventId) => ({...result, [eventIdHash(eventId)]: true}),
            this.trackedEventHashMap,
        );

        this.saveTrackedEventHashMap();

        const dedupedFailures = dedupedFailuresMap.values();

        this.failuresToTrack = [...this.failuresToTrack, ...dedupedFailures];
    }

    /**
     * If there is a failure that should be tracked, call the given trackDecryptionFailure
     * function with the first failure in the FIFO of failures that should be tracked.
     */
    trackFailure() {
        if (this.failuresToTrack.length > 0) {
            this.trackDecryptionFailure(this.failuresToTrack.shift());
        }
    }
}
<|endoftext|>


<commit_before>//
// Ibisense device data daemon
// Ibisense (c) 2013-
//
"use strict";

global.jQuery = require('jquery');

var sys = require('sys');
var spawn = require('child_process').spawn;
var fs = require('fs');
var ibisense = require("../lib/ibisense.api.js");
var channelSlugs = {};

//Configuration
var config = JSON.parse(fs.readFileSync("../etc/ibideviced_config.json"));

var sensorid = config.sensorid;

//Init Ibisense API
ibisense.setApiKey(config.apikey);

//Logging
var log4js = require("log4js");
log4js.loadAppender('file');
//log4js.addAppender(log4js.appenders.console());
log4js.addAppender(log4js.appenders.file(config.log_dir+'/ibideviced.log'), 'ibideviced');
var log = log4js.getLogger("ibideviced");
//var log = console;
if(true) {
    log.debug = sys.debug;
} else {
    log.debug = function(){};
}

//Here are the data collecting scripts
var collectors_path = config.collectors_dir;
if(collectors_path[collectors_path.length-1] !== '/') {
    collectors_path = collectors_path + "/";
}

//TODO: add data sinks

var collector_runnable = function(name) {
    //TODO: add check for executable and non-directory
    //Do not run backup files
    if(name.match(/^\.$/) || name.match(/\#/) || name.match(/~$/)) {
	return false;
    } else {
	return true;
    }
}

var runCollectors = function() {
    try {
	var collectors = fs.readdirSync(collectors_path);
    } catch (e) {
	//TODO: add exception handling
    }

    for (var c_idx in collectors) {
	var collector = collectors[c_idx];
	log.debug(collector);
	if(collector_runnable(collector)) {
	    execCollector(collectors_path + collector, null);
	}
    }
};

var measurementNonexistentChannel = function(chName, m) {
    //Send new channel request
}

//Look up CUID for a measurement
var loadChannels = function() {
    ibisense.channels.list(sensorid,
        function(channels) {
             for (var i in channels) {
                 var ch = channels[i];
                 log.info("Data from sensor '" + ch.abbreviation() + "' will be stored to channel '" + ch.cuid() + "'");
                 channelSlugs[ch.abbreviation()] = ch.cuid();
             }
        }, function(code) {
            log.error("Error on loading channel data from Ibisense, status=" + status);
        }
    );
}

loadChannels();

//Create new channel
var createChannel = function(ts, chName, chValue) {
    var ch = new ibisense.models.Channel({
	    "name" : "RPi sensor " + chName,
	    "description" : "",
	    "unit" : "",
	    "abbreviation" : chName
	});

    log.error("New channel: " + ch.toJsonString());

    ibisense.channels.add(sensorid,
			  ch,
			  function(newch, status) {
			      //New channel created - store CUID and repost data
			      log.info("Created new channel '" + newch.cuid() + "' for sensor '" + chName + "'");
			      channelSlugs[chName] = newch.cuid();
			      measurementReceived(ts, chName, chValue);
			  },
			  function () {
			      //Error
			      log.error("Can't create new channel " + chName);
			  });
    ;

}

//Send measurement to Ibisense
var measurementReceived = function(ts,chName,chValue)  {
    var m = { 'abs_time' : ts,
	      'value' : chValue };

    if(channelSlugs[chName]) {
	//If the CUID is known, send the data to Ibisense
	ibiSendMeasurement(channelSlugs[chName], m);
    } else {
	//If CUID is unknown, poll for it. If not known, create channel
	//measurementUnknownCUID(chname, m);
	log.warn("Unknown CUID for " + chName);
	createChannel(ts, chName, chValue);
    }
}

//Process data formats like:
// T,20

// T1,20.34 °C
// T2,34.22 °C

// V1,12.00V
// RH,34 %

// Use * to denote measurement timestamp
// *02:03:00,T,20
// *02:03:01,T,20

var processCollectorResult = function(path,data) {
    //var lines = data.split("\n");
    var lines = data;
    for (var lineno in lines) {
	var ts = null;
	var line = lines[lineno];
	var lineItems = line.split(",");
	var ts = new Date();
        log.debug("Received new measurement: " + line)
	for(var i in lineItems) {

	    //Is the first one a timestamp ?
	    if (i===0 && lineItems[i].match(/^\*/)) {
		try {
		    ts = new Date(lineItems[0].substring(1));
		} catch (e) {
		    //Error in timestamp
		log.error(path + " can't parse timestamp " + lineItems[0]);
		ts=null;
		}
	    } else {
		//This is a measurement, parse it
		var m, chName, chValue;
		var ll = lineItems[i];

		if(m=ll.match(/^([^,]+?)\s+([+-]?.*)/)) {
		    if(m.length >=3) {
			var chName = m[1];
			var chValue = parseFloat(m[2]);
                        log.debug("line: " + ll + ", chName: " + chName + ", chValue: " + chValue);
                        if (chName && !isNaN(chValue)) {
  	                    measurementReceived(ts,chName,chValue);
                        }
		    }
		    else {
			log.error(path + " value " + lineItems[i] + " does not contain channel name and unit");
		    }
		} else {
		    log.error("Unable to parse line " + line);
		}

	    }

	}

    }
}

var execCollector = function(path, sink) {
    var running = true;
    var stdout_data=[];
    log.info("Starting " + path);
    //TODO: adddata sink

    var child = spawn(path);

    child.stdout.on('data', function(d) {
	    //Collect data from STDOUT
	    stdout_data.push(""+d);
	    log.debug("STDOUT " + stdout_data);
	});

    child.stderr.on('data', function(d) {
	    log.error(path + " stderr: " + d);
	});

    child.on('close', function(code, signal) {
	    running = false;
	    if(code === 0) {
		processCollectorResult(path, stdout_data);
	    } else {
		log.warn(path + " closed with code " + code);
	    }
	});

    child.on('close', function(code, signal) {
	    log.debug(path + " exit: " + code + "/" + signal); running = false;
	});

    //TODO: add timeout+kill
    setTimeout(function() {
	    if(running) {
		child.kill("SIGTERM");
		log.warn("Sleeping process,killed " + path );
	    }
	}, config.process_timeout);
};

setInterval(
    function() {
	runCollectors();
    }, 5000
);

//TODO: add data buffering if

// Message handling routines
var ibiSendMeasurement = function(slug, m) {
    var cuid = '';

    if(typeof m.value !== "number") {
	//is it a stringed number ?
	if(!isNaN(parseFloat(m.value))) {
	    //Yes, replace with parsed number
	    m.value = parseFloat(m.value);
	} else {
	    //Only numeric data allowed currently
	    log.debug("Non-numeric measurement: " + m.value);
	    return;
	}
    }

    var dp = new ibisense.models.DataPoint({ "date": m.abs_time,
					     "value": m.value });

    log.debug("Storing " + dp.toJsonString());

    var onError = function(status) {
	log.error("Ibisense store failed with code " + status);
    };

    var onSuccess = function(status) {
	log.debug("Ibisense store OK");
    };

    var onErrorGen = function(dpoint) {
	return function(status) {
	    log.error("Ibisense store failed with code " + status + " for data " + dpoint);
	}
    }

    var onSuccessGen = function (dpoint) {
	return function(status) {
	    console.log("Stored: " + dpoint);
	};
    };

    if(typeof slug !== 'undefined' && slug && dp.toJson().t && dp.toJson().v) {
	log.debug("Storing " + slug + " = " + dp.toJsonString());
	ibisense.datapoints.add(slug,
				[dp],
				onSuccessGen(slug + "=" +dp.toJsonString()),
				onErrorGen(slug + "=" + dp.toJsonString()),
				null);
    } else {
	    log.warn("Slug not defined for " + m.nodename);
    }
}

<commit_msg>Changed debug-logging to use log4js TRACE-level<commit_after>//
// Ibisense device data daemon
// Ibisense (c) 2013-
//
"use strict";

global.jQuery = require('jquery');

var sys = require('sys');
var spawn = require('child_process').spawn;
var fs = require('fs');
var ibisense = require("../lib/ibisense.api.js");
var channelSlugs = {};

//Configuration
var config = JSON.parse(fs.readFileSync("../etc/ibideviced_config.json"));

var sensorid = config.sensorid;

//Init Ibisense API
ibisense.setApiKey(config.apikey);

//Logging
var log4js = require("log4js");
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file(config.log_dir+'/ibideviced.log'), 'ibideviced');
var log = log4js.getLogger("ibideviced");
log.setLevel(log4js.levels.INFO);

//Debug on / off ?
log.setLevel(log4js.levels.TRACE)

//Here are the data collecting scripts
var collectors_path = config.collectors_dir;
if(collectors_path[collectors_path.length-1] !== '/') {
    collectors_path = collectors_path + "/";
}

//TODO: add data sinks

var collector_runnable = function(name) {
    //TODO: add check for executable and non-directory
    //Do not run backup files
    if(name.match(/^\.$/) || name.match(/\#/) || name.match(/~$/)) {
	return false;
    } else {
	return true;
    }
}

var runCollectors = function() {
    try {
	var collectors = fs.readdirSync(collectors_path);
    } catch (e) {
	//TODO: add exception handling
    }

    for (var c_idx in collectors) {
	var collector = collectors[c_idx];
	log.trace(collector);
	if(collector_runnable(collector)) {
	    execCollector(collectors_path + collector, null);
	}
    }
};

var measurementNonexistentChannel = function(chName, m) {
    //Send new channel request
}

//Look up CUID for a measurement
var loadChannels = function() {
    ibisense.channels.list(sensorid,
        function(channels) {
             for (var i in channels) {
                 var ch = channels[i];
                 log.info("Data from sensor '" + ch.abbreviation() + "' will be stored to channel '" + ch.cuid() + "'");
                 channelSlugs[ch.abbreviation()] = ch.cuid();
             }
        }, function(code) {
            log.error("Error on loading channel data from Ibisense, status=" + code);
        }
    );
}

loadChannels();

//Create new channel
var createChannel = function(ts, chName, chValue) {
    var ch = new ibisense.models.Channel({
	    "name" : "RPi sensor " + chName,
	    "description" : "",
	    "unit" : "",
	    "abbreviation" : chName
	});

    log.error("New channel: " + ch.toJsonString());

    ibisense.channels.add(sensorid,
			  ch,
			  function(newch, status) {
			      //New channel created - store CUID and repost data
			      log.info("Created new channel '" + newch.cuid() + "' for sensor '" + chName + "'");
			      channelSlugs[chName] = newch.cuid();
			      measurementReceived(ts, chName, chValue);
			  },
			  function () {
			      //Error
			      log.error("Can't create new channel " + chName);
			  });
    ;

}

//Send measurement to Ibisense
var measurementReceived = function(ts,chName,chValue)  {
    var m = { 'abs_time' : ts,
	      'value' : chValue };

    if(channelSlugs[chName]) {
	//If the CUID is known, send the data to Ibisense
	ibiSendMeasurement(channelSlugs[chName], m);
    } else {
	//If CUID is unknown, poll for it. If not known, create channel
	//measurementUnknownCUID(chname, m);
	log.warn("Unknown CUID for " + chName);
	createChannel(ts, chName, chValue);
    }
}

//Process data formats like:
// T,20

// T1,20.34 °C
// T2,34.22 °C

// V1,12.00V
// RH,34 %

// Use * to denote measurement timestamp
// *02:03:00,T,20
// *02:03:01,T,20

var processCollectorResult = function(path,data) {
    //var lines = data.split("\n");
    var lines = data;
    for (var lineno in lines) {
	var ts = null;
	var line = lines[lineno];
	var lineItems = line.split(",");
	var ts = new Date();
        log.trace("Received new measurement: " + line)
	for(var i in lineItems) {

	    //Is the first one a timestamp ?
	    if (i===0 && lineItems[i].match(/^\*/)) {
		try {
		    ts = new Date(lineItems[0].substring(1));
		} catch (e) {
		    //Error in timestamp
		log.error(path + " can't parse timestamp " + lineItems[0]);
		ts=null;
		}
	    } else {
		//This is a measurement, parse it
		var m, chName, chValue;
		var ll = lineItems[i];

		if(m=ll.match(/^([^,]+?)\s+([+-]?.*)/)) {
		    if(m.length >=3) {
			var chName = m[1];
			var chValue = parseFloat(m[2]);
                        log.trace("line: " + ll + ", chName: " + chName + ", chValue: " + chValue);
                        if (chName && !isNaN(chValue)) {
  	                    measurementReceived(ts,chName,chValue);
                        }
		    }
		    else {
			log.error(path + " value " + lineItems[i] + " does not contain channel name and unit");
		    }
		} else {
		    log.error("Unable to parse line " + line);
		}

	    }

	}

    }
}

var execCollector = function(path, sink) {
    var running = true;
    var stdout_data=[];
    log.info("Starting " + path);
    //TODO: adddata sink

    var child = spawn(path);

    child.stdout.on('data', function(d) {
	    //Collect data from STDOUT
	    stdout_data.push(""+d);
	    log.trace("STDOUT " + stdout_data);
	});

    child.stderr.on('data', function(d) {
	    log.error(path + " stderr: " + d);
	});

    child.on('close', function(code, signal) {
	    running = false;
	    if(code === 0) {
		processCollectorResult(path, stdout_data);
	    } else {
		log.warn(path + " closed with code " + code);
	    }
	});

    child.on('close', function(code, signal) {
	    log.trace(path + " exit: " + code + "/" + signal); running = false;
	});

    //TODO: add timeout+kill
    setTimeout(function() {
	    if(running) {
		child.kill("SIGTERM");
		log.warn("Sleeping process,killed " + path );
	    }
	}, config.process_timeout);
};

setInterval(
    function() {
	runCollectors();
    }, 5000
);

//TODO: add data buffering if

// Message handling routines
var ibiSendMeasurement = function(slug, m) {
    var cuid = '';

    if(typeof m.value !== "number") {
	//is it a stringed number ?
	if(!isNaN(parseFloat(m.value))) {
	    //Yes, replace with parsed number
	    m.value = parseFloat(m.value);
	} else {
	    //Only numeric data allowed currently
	    log.trace("Non-numeric measurement: " + m.value);
	    return;
	}
    }

    var dp = new ibisense.models.DataPoint({ "date": m.abs_time,
					     "value": m.value });

    log.trace("Storing " + dp.toJsonString());

    var onError = function(status) {
	log.error("Ibisense store failed with code " + status);
    };

    var onSuccess = function(status) {
	log.trace("Ibisense store OK");
    };

    var onErrorGen = function(dpoint) {
	return function(status) {
	    log.error("Ibisense store failed with code " + status + " for data " + dpoint);
	}
    }

    var onSuccessGen = function (dpoint) {
	return function(status) {
	    log.trace("Stored: " + dpoint);
	};
    };

    if(typeof slug !== 'undefined' && slug && dp.toJson().t && dp.toJson().v) {
	log.trace("Storing " + slug + " = " + dp.toJsonString());
	ibisense.datapoints.add(slug,
				[dp],
				onSuccessGen(slug + "=" +dp.toJsonString()),
				onErrorGen(slug + "=" + dp.toJsonString()),
				null);
    } else {
	    log.warn("Slug not defined for " + m.nodename);
    }
}

<|endoftext|>


<commit_before>// Modified in tools.js from selenium-IDE

function TargetSelecter(callback, cleanupCallback) {
    this.callback = callback;
    this.cleanupCallback = cleanupCallback;

    // This is for XPCOM/XUL addon and can't be used
    //var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
    //this.win = wm.getMostRecentWindow('navigator:browser').getBrowser().contentWindow;

    // Instead, we simply assign global content window to this.win
    this.win = window;
    var doc = this.win.document;
    var div = doc.createElement("div");
    div.setAttribute("style", "display: none;");
    doc.body.insertBefore(div, doc.body.firstChild);
    this.div = div;
    this.e = null;
    this.r = null;
    doc.addEventListener("mousemove", this, true);
    doc.addEventListener("click", this, true);
}

TargetSelecter.prototype.cleanup = function () {
    try {
        if (this.div) {
            if (this.div.parentNode) {
                this.div.parentNode.removeChild(this.div);
            }
            this.div = null;
        }
        if (this.win) {
            var doc = this.win.document;
            doc.removeEventListener("mousemove", this, true);
            doc.removeEventListener("click", this, true);
        }
    } catch (e) {
        if (e != "TypeError: can't access dead object") {
            throw e;
        }
    }
    this.win = null;
    if (this.cleanupCallback) {
        this.cleanupCallback();
    }
};

TargetSelecter.prototype.handleEvent = function (evt) {
    switch (evt.type) {
        case "mousemove":
            this.highlight(evt.target.ownerDocument, evt.clientX, evt.clientY);
            break;
        case "click":
            if (evt.button == 0 && this.e && this.callback) {
                this.callback(this.e, this.win);
            } //Right click would cancel the select
            evt.preventDefault();
            evt.stopPropagation();
            this.cleanup();
            break;
    }
};

TargetSelecter.prototype.highlight = function (doc, x, y) {
    if (doc) {
        var e = doc.elementFromPoint(x, y);
        if (e && e != this.e) {
            this.highlightElement(e);
        }
    }
}

TargetSelecter.prototype.highlightElement = function (element) {
    if (element && element != this.e) {
        this.e = element;
    } else {
        return;
    }
    var r = element.getBoundingClientRect();
    var or = this.r;
    if (r.left >= 0 && r.top >= 0 && r.width > 0 && r.height > 0) {
        if (or && r.top == or.top && r.left == or.left && r.width == or.width && r.height == or.height) {
            return;
        }
        this.r = r;
        var style = "pointer-events: none; position: absolute; box-shadow: 0 0 0 1px black; outline: 1px dashed white; outline-offset: -1px; background-color: rgba(250,250,128,0.4); z-index: 100;";
        var pos = "top:" + (r.top + this.win.scrollY) + "px; left:" + (r.left + this.win.scrollX) + "px; width:" + r.width + "px; height:" + r.height + "px;";
        this.div.setAttribute("style", style + pos);
    } else if (or) {
        this.div.setAttribute("style", "display: none;");
    }
};<commit_msg>linted and fixed typo<commit_after>// Modified in tools.js from selenium-IDE

function TargetSelector(callback, cleanupCallback) {
  this.callback = callback;
  this.cleanupCallback = cleanupCallback;

  // This is for XPCOM/XUL addon and can't be used
  //var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
  //this.win = wm.getMostRecentWindow('navigator:browser').getBrowser().contentWindow;

  // Instead, we simply assign global content window to this.win
  this.win = window;
  const doc = this.win.document;
  const div = doc.createElement("div");
  div.setAttribute("style", "display: none;");
  doc.body.insertBefore(div, doc.body.firstChild);
  this.div = div;
  this.e = null;
  this.r = null;
  doc.addEventListener("mousemove", this, true);
  doc.addEventListener("click", this, true);
}

TargetSelector.prototype.cleanup = function () {
  try {
    if (this.div) {
      if (this.div.parentNode) {
        this.div.parentNode.removeChild(this.div);
      }
      this.div = null;
    }
    if (this.win) {
      const doc = this.win.document;
      doc.removeEventListener("mousemove", this, true);
      doc.removeEventListener("click", this, true);
    }
  } catch (e) {
    if (e != "TypeError: can't access dead object") {
      throw e;
    }
  }
  this.win = null;
  if (this.cleanupCallback) {
    this.cleanupCallback();
  }
};

TargetSelector.prototype.handleEvent = function (evt) {
  switch (evt.type) {
    case "mousemove":
      this.highlight(evt.target.ownerDocument, evt.clientX, evt.clientY);
      break;
    case "click":
      if (evt.button == 0 && this.e && this.callback) {
        this.callback(this.e, this.win);
      } //Right click would cancel the select
      evt.preventDefault();
      evt.stopPropagation();
      this.cleanup();
      break;
  }
};

TargetSelector.prototype.highlight = function (doc, x, y) {
  if (doc) {
    const e = doc.elementFromPoint(x, y);
    if (e && e != this.e) {
      this.highlightElement(e);
    }
  }
};

TargetSelector.prototype.highlightElement = function (element) {
  if (element && element != this.e) {
    this.e = element;
  } else {
    return;
  }
  const r = element.getBoundingClientRect();
  const or = this.r;
  if (r.left >= 0 && r.top >= 0 && r.width > 0 && r.height > 0) {
    if (or && r.top == or.top && r.left == or.left && r.width == or.width && r.height == or.height) {
      return;
    }
    this.r = r;
    const style = "pointer-events: none; position: absolute; box-shadow: 0 0 0 1px black; outline: 1px dashed white; outline-offset: -1px; background-color: rgba(250,250,128,0.4); z-index: 100;";
    const pos = "top:" + (r.top + this.win.scrollY) + "px; left:" + (r.left + this.win.scrollX) + "px; width:" + r.width + "px; height:" + r.height + "px;";
    this.div.setAttribute("style", style + pos);
  } else if (or) {
    this.div.setAttribute("style", "display: none;");
  }
};
<|endoftext|>


<commit_before>/*global window: false */

/**
 * General utility functions used by Kanso. Some functions were moved here from
 * other modules (such as core), to avoid a circular dependency bug in CouchDB.
 */

/**
 * Module dependencies
 */

var settings = require('./settings'); // settings module is auto-generated

/**
 * Some functions calculate results differently depending on the execution
 * environment. The isBrowser value is used to set the correct environment
 * for these functions, and is only exported to make unit testing easier.
 *
 * You should not need to change this value during normal usage.
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and db.js ...once circular dependencies in
// couchdb's commonjs implementation are fixed it can be moved back into
// core.js. For now, this is also exported from core.js and should
// be accessed from there.
exports.isBrowser = false;
if (typeof window !== 'undefined') {
    exports.isBrowser = true;
}

/**
 * This is because the first page hit also triggers kanso to handle the url
 * client-side. Knowing it is the first page being loaded means we can stop
 * the pageTracker code from submitting the URL twice. Exported because this
 * might be useful information to other modules, it should not be modified
 * by them.
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and session.js
exports.initial_hit = true;

/**
 * Used to store userCtx, periodically updated like on session.login and
 * session.logout. TODO: Or if a permissions error is returned from a db method?
 */

// TODO: added to utils to avoid circular dependency bug in couchdb
exports.userCtx = null;

/**
 * Caches extended session info (like the current authentication db) after
 * a call to session.info
 */
exports.session = null;

/**
 * Returns the path to prefix to any URLs. When running behind a
 * virtual host, there is nothing to prefix URLs with. When accessing the
 * app directly, URLs need to be prefixed with /db/_design/appname/_rewrite.
 *
 * The request object argument is only required when run server-side.
 *
 * @param {Object} req
 * @returns {String}
 * @api public
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and db.js ...once circular dependencies in
// couchdb's commonjs implementation are fixed it can be moved back into
// core.js. For now, this is also exported from core.js and should
// be accessed from there.
exports.getBaseURL = function (req) {
    if ('baseURL' in settings) {
        return settings.baseURL;
    }
    if (exports.isBrowser) {
        var re = new RegExp('(.*\\/_rewrite).*$');
        var match = re.exec(window.location.pathname);
        if (match) {
            return match[1];
        }
        return '';
    }
    if (req.headers['x-couchdb-vhost-path']) {
        return '';
    }
    return '/' + req.path.slice(0, 3).join('/') + '/_rewrite';
};

/**
 * Traverses an object and its sub-objects using an array of property names.
 * Returns the value of the matched path, or undefined if the property does not
 * exist.
 *
 * getPropertyPath({a: {b: 'foo'}}, ['a','b']) -> 'foo'
 *
 * @param {Object} obj
 * @param {Array} path
 */

exports.getPropertyPath = function (obj, path) {
    if (!path.length || !obj) {
        return obj;
    }
    return exports.getPropertyPath(obj[path[0]], path.slice(1));
};

/**
 * Traverses an object and its sub-objects using an array of property names.
 * Sets the value of the matched property.
 *
 * setPropertyPath({}, ['a','b'], 'foo') -> {a: {b: 'foo'}}
 *
 * @param {Object} obj
 * @param {Array} path
 * @returns undefined
 */

exports.setPropertyPath = function (obj, path, val) {
    if (!path.length) {
        throw new Error('No property path given');
    }
    if (path.length === 1) {
        obj[path[0]] = val;
        return;
    }
    var next = path[0];
    path = path.slice(1);
    if (obj[next] === undefined) {
        obj[next] = {};
    }
    else if (typeof obj[next] !== 'object' && path.length) {
        throw new Error('Property path conflicts with existing value');
    }
    exports.setPropertyPath(obj[next], path, val);
};

/**
 * Returns the name of the constructor function for an object. This is used
 * as a workaround for CouchDB's lack of a module cache, where instanceof checks
 * can break if a module is re-eval'd.
 *
 * @param {Object} obj
 * @returns {String}
 */

exports.constructorName = function (obj) {
    if (obj === null || obj === undefined) {
        return undefined;
    }
    if (obj.constructor.name) {
        return obj.constructor.name;
    }
    var match = new RegExp('function (.+)\\(').exec(obj.constructor.toString());
    return (match && match.length > 1) ? match[1] : undefined;
};

/**
 * Call function with arguments, catch any errors and add to an array,
 * returning the modified array.
 *
 * @param {Array} arr
 * @param {Function} fn
 * @param {Array} args
 * @return {Array}
 * @api private
 */

exports.getErrors = function (fn, args) {
    var arr = [];
    try {
        arr = arr.concat(fn.apply(this, args) || []);
    }
    catch (e) {
        arr.push(e);
    }
    return arr;
};

exports.escapeHTML = function (s) {
    // coerce to string
    s = '' + s;
    s = s.replace(/&/g, '&');
    s = s.replace(/</g, '<');
    s = s.replace(/>/g, '>');
    s = s.replace(/"/g, '"');
    return s;
};
<commit_msg>added parseCSV function to utils, towards #58<commit_after>/*global window: false */

/**
 * General utility functions used by Kanso. Some functions were moved here from
 * other modules (such as core), to avoid a circular dependency bug in CouchDB.
 */

/**
 * Module dependencies
 */

var settings = require('./settings'); // settings module is auto-generated

/**
 * Some functions calculate results differently depending on the execution
 * environment. The isBrowser value is used to set the correct environment
 * for these functions, and is only exported to make unit testing easier.
 *
 * You should not need to change this value during normal usage.
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and db.js ...once circular dependencies in
// couchdb's commonjs implementation are fixed it can be moved back into
// core.js. For now, this is also exported from core.js and should
// be accessed from there.
exports.isBrowser = false;
if (typeof window !== 'undefined') {
    exports.isBrowser = true;
}

/**
 * This is because the first page hit also triggers kanso to handle the url
 * client-side. Knowing it is the first page being loaded means we can stop
 * the pageTracker code from submitting the URL twice. Exported because this
 * might be useful information to other modules, it should not be modified
 * by them.
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and session.js
exports.initial_hit = true;

/**
 * Used to store userCtx, periodically updated like on session.login and
 * session.logout. TODO: Or if a permissions error is returned from a db method?
 */

// TODO: added to utils to avoid circular dependency bug in couchdb
exports.userCtx = null;

/**
 * Caches extended session info (like the current authentication db) after
 * a call to session.info
 */
exports.session = null;

/**
 * Returns the path to prefix to any URLs. When running behind a
 * virtual host, there is nothing to prefix URLs with. When accessing the
 * app directly, URLs need to be prefixed with /db/_design/appname/_rewrite.
 *
 * The request object argument is only required when run server-side.
 *
 * @param {Object} req
 * @returns {String}
 * @api public
 */

// TODO: this was moved to this module from core.js to avoid a circular
// dependency between core.js and db.js ...once circular dependencies in
// couchdb's commonjs implementation are fixed it can be moved back into
// core.js. For now, this is also exported from core.js and should
// be accessed from there.
exports.getBaseURL = function (req) {
    if ('baseURL' in settings) {
        return settings.baseURL;
    }
    if (exports.isBrowser) {
        var re = new RegExp('(.*\\/_rewrite).*$');
        var match = re.exec(window.location.pathname);
        if (match) {
            return match[1];
        }
        return '';
    }
    if (req.headers['x-couchdb-vhost-path']) {
        return '';
    }
    return '/' + req.path.slice(0, 3).join('/') + '/_rewrite';
};

/**
 * Traverses an object and its sub-objects using an array of property names.
 * Returns the value of the matched path, or undefined if the property does not
 * exist.
 *
 * getPropertyPath({a: {b: 'foo'}}, ['a','b']) -> 'foo'
 *
 * @param {Object} obj
 * @param {Array} path
 */

exports.getPropertyPath = function (obj, path) {
    if (!path.length || !obj) {
        return obj;
    }
    return exports.getPropertyPath(obj[path[0]], path.slice(1));
};

/**
 * Traverses an object and its sub-objects using an array of property names.
 * Sets the value of the matched property.
 *
 * setPropertyPath({}, ['a','b'], 'foo') -> {a: {b: 'foo'}}
 *
 * @param {Object} obj
 * @param {Array} path
 * @returns undefined
 */

exports.setPropertyPath = function (obj, path, val) {
    if (!path.length) {
        throw new Error('No property path given');
    }
    if (path.length === 1) {
        obj[path[0]] = val;
        return;
    }
    var next = path[0];
    path = path.slice(1);
    if (obj[next] === undefined) {
        obj[next] = {};
    }
    else if (typeof obj[next] !== 'object' && path.length) {
        throw new Error('Property path conflicts with existing value');
    }
    exports.setPropertyPath(obj[next], path, val);
};

/**
 * Returns the name of the constructor function for an object. This is used
 * as a workaround for CouchDB's lack of a module cache, where instanceof checks
 * can break if a module is re-eval'd.
 *
 * @param {Object} obj
 * @returns {String}
 */

exports.constructorName = function (obj) {
    if (obj === null || obj === undefined) {
        return undefined;
    }
    if (obj.constructor.name) {
        return obj.constructor.name;
    }
    var match = new RegExp('function (.+)\\(').exec(obj.constructor.toString());
    return (match && match.length > 1) ? match[1] : undefined;
};

/**
 * Call function with arguments, catch any errors and add to an array,
 * returning the modified array.
 *
 * @param {Array} arr
 * @param {Function} fn
 * @param {Array} args
 * @return {Array}
 * @api private
 */

exports.getErrors = function (fn, args) {
    var arr = [];
    try {
        arr = arr.concat(fn.apply(this, args) || []);
    }
    catch (e) {
        arr.push(e);
    }
    return arr;
};

exports.escapeHTML = function (s) {
    // coerce to string
    s = '' + s;
    s = s.replace(/&/g, '&');
    s = s.replace(/</g, '<');
    s = s.replace(/>/g, '>');
    s = s.replace(/"/g, '"');
    return s;
};

// Parsing comma-separated values (CSV) in JavaScript by M. A. SRIDHAR
// http://yawgb.blogspot.com/2009/03/parsing-comma-separated-values-in.html

exports.parseCSV = function (csvString) {
    var fieldEndMarker  = /([,\015\012] *)/g;
    var qFieldEndMarker = /("")*"([,\015\012] *)/g;
    var startIndex = 0;
    var records = [], currentRecord = [];
    do {
        var ch = csvString.charAt(startIndex);
        var endMarkerRE = (ch === '"') ? qFieldEndMarker : fieldEndMarker;
        endMarkerRE.lastIndex = startIndex;
        var matchArray = endMarkerRE.exec(csvString);
        if (!matchArray || !matchArray.length) {
            break;
        }
        var endIndex = endMarkerRE.lastIndex;
        endIndex -= matchArray[matchArray.length - 1].length;
        var match = csvString.substring(startIndex, endIndex);
        if (match.charAt(0) === '"') {
            match = match.substring(1, match.length - 1).replace(/""/g, '"');
        }
        currentRecord.push(match);
        var marker = matchArray[0];
        if (marker.indexOf(',') < 0) {
            records.push(currentRecord);
            currentRecord = [];
        }
        startIndex = endMarkerRE.lastIndex;
    } while (true);
    if (startIndex < csvString.length) {
        var remaining = csvString.substring(startIndex).trim();
        if (remaining) {
            currentRecord.push(remaining);
        }
    }
    if (currentRecord.length > 0) {
        records.push(currentRecord);
    }
    return records;
};
<|endoftext|>