<commit_before>/*
* Display calendar UI for datepicker.
* Sync the cursor to the selected date or default to today.
* Month is rendered based on cursor date.
* Handles date math via moment.
*/

import React from 'react';
import { intlShape } from 'react-intl';
import PropTypes from 'prop-types';
import Moment from 'moment';
import { extendMoment } from 'moment-range';
import DayButton from './DayButton';
import Icon from '../Icon';

import css from './Calendar.css';

const moment = extendMoment(Moment);

function getCalendar(year, month) {
  const startDate = moment([year, month]);
  const firstDay = moment(startDate).startOf('month');
  const endDay = moment(startDate).endOf('month');
  const monthRange = moment.range(firstDay, endDay);

  const weeks = [];
  const calendar = [];
  const rowStartArray = [];
  const rowEndArray = [];

  const weekdays = Array.from(monthRange.by('days'));
  weekdays.forEach((mo) => {
    const ref = mo.week();
    if (weeks.indexOf(ref) < 0) {
      weeks.push(mo.week());
      const endClone = moment(mo);
      rowStartArray.push(mo.weekday(0));
      rowEndArray.push(endClone.weekday(6));
    }
  });

  for (let i = 0; i < weeks.length; i += 1) {
    const weekRange = moment.range(rowStartArray[i], rowEndArray[i]);
    calendar.push(weekRange);
  }

  return calendar;
}

const propTypes = {
  onSetDate: PropTypes.func,
  dateFormat: PropTypes.string,
  selectedDate: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
  locale: PropTypes.string,
  onBlur: PropTypes.func,
  onKeyDown: PropTypes.func,
  buttonFocus: PropTypes.func,
  id: PropTypes.string,
  excludeDates: PropTypes.oneOfType([
    PropTypes.array,
    PropTypes.object,
    PropTypes.string,
  ]),
};

const defaultProps = {
  dateFormat: 'MM DD YYYY',
  onSetDate: () => null,
  locale: 'en',
};

const contextTypes = {
  intl: intlShape
};

class Calendar extends React.Component {
  constructor(props) {
    super(props);

    moment.locale(this.props.locale);

    let inputDate; // initial date for state
    let cursorDate;
    if (this.props.selectedDate === '' || this.props.selectedDate === 'undefined') {
      inputDate = null;
      cursorDate = moment();
    } else if (moment(this.props.selectedDate, this.props.dateFormat, true).isValid()) {
      inputDate = moment(this.props.selectedDate, this.props.dateFormat, true);
      cursorDate = inputDate;
    } else {
      inputDate = moment();
      cursorDate = moment();
    }

    const base = moment(cursorDate);
    const month = base.month();
    const year = base.year();

    this.state = {
      cursorDate,
      date: inputDate,
      month,
      year,
      calendar: getCalendar(year, month),
    };

    this.handleDayClick = this.handleDayClick.bind(this);
    this.setDate = this.setDate.bind(this);
    this.nextMonth = this.nextMonth.bind(this);
    this.previousMonth = this.previousMonth.bind(this);
    this.nextYear = this.nextYear.bind(this);
    this.previousYear = this.previousYear.bind(this);
    this.checkExcluded = this.checkExcluded.bind(this);
    this.isDateSelected = this.isDateSelected.bind(this);
    this.updateCursorDate = this.updateCursorDate.bind(this);
    this.getDOMContainer = this.getDOMContainer.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    let stateUpdate;
    if (nextProps.selectedDate !== this.props.selectedDate) {
      const moDate = moment(nextProps.selectedDate, this.props.dateFormat, true);
      if (moDate.isValid()) {
        if (moDate !== this.state.date) {
          // const moDate = moment(nextProps.selectedDate);
          const month = moDate.month();
          const year = moDate.year();
          stateUpdate = {
            cursorDate: moDate,
            date: moDate,
            month,
            year,
            calendar: getCalendar(year, month),
          };
        }
      } else { // fix navigation issue on null or invalid date
        const fallbackDate = moment();
        const month = fallbackDate.month();
        const year = fallbackDate.year();
        stateUpdate = {
          date: fallbackDate,
          month,
          year,
          calendar: getCalendar(year, month),
        };
      }
    }

    if (stateUpdate) {
      this.setState((oldState) => {
        const newState = oldState;
        Object.assign(newState, stateUpdate);
        newState.calendar = getCalendar(newState.year, newState.month);
        return newState;
      });
    }
  }

  setDate(day) {
    this.setState({
      date: day,
    });
  }

  getDOMContainer() {
    return this._calendarContainer;
  }

  getMonth() {
    return this.state.month;
  }

  getCursorDate() {
    return this.state.cursorDate;
  }

  previousYear(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'y');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextYear(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'y');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousMonth(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'M');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextMonth(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'M');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextDay(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousDay(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextWeek(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(7, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousWeek(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(7, 'days');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  updateCursorDate(date) {
    this.setState((oldState) => {
      const newState = Object.assign({}, oldState);
      newState.cursorDate = date;
      newState.month = newState.cursorDate.month();
      newState.year = newState.cursorDate.year();
      if (newState.year !== oldState.year || newState.month !== oldState.month) {
        newState.calendar = getCalendar(newState.year, newState.month);
      }
      return newState;
    });
  }

  handleDayClick(day, e) {
    if (e) { e.preventDefault(); }
    this.setDate(day);
    this.props.onSetDate(e, day, day.format(this.props.dateFormat));
  }

  checkExcluded(date) {
    let excluded = false;
    if (!this.props.excludeDates) {
      return false;
    }

    const ex = this.props.excludeDates;
    switch (typeof (ex)) {
      case 'string':
        if (date.isSame(moment(ex), 'day')) {
          return true;
        }
        break;
      case 'object':
        if (moment.isMoment(ex)) {
          if (date.isSame(ex, 'day')) {
            return true;
          }
        } else { // excluded is an array...
          ex.forEach((exd) => {
            if (moment.isMoment(exd)) {
              if (date.isSame(exd, 'day')) {
                excluded = true;
              }
            } else if (date.isSame(moment(exd), 'day')) {
              excluded = true;
            }
          });
        }
        break;
      default:
        return false;
    }
    return excluded;
  }

  isDateSelected(day) {
    if (this.props.selectedDate) {
      return day.format(this.props.dateFormat) ===
        moment(this.props.selectedDate, this.props.dateFormat, true).format(this.props.dateFormat);
    }
    if (this.state.date) {
      return day.format(this.props.dateFormat) ===
        moment(this.state.date, this.props.dateFormat, true).format(this.props.dateFormat);
    }
    return false;
  }

  render() {
    const { id } = this.props;
    const formatMsg = this.context.intl.formatMessage;
    let weekCount = 0;
    const weeks = this.state.calendar.map(
      (week) => {
        weekCount += 1;
        const dayList = [];
        const weekDays = Array.from(week.by('days'));
        weekDays.forEach((day) => { dayList.push(day); });

        const days = dayList.map(
          (day) => {
            const isCurrentMonth = day.month() === this.state.month;
            const isToday = day.isSame(moment(), 'day');
            const isSelected = this.isDateSelected(day);
            const isCursored = day.isSame(this.state.cursorDate, 'day');
            const isExcluded = this.checkExcluded(day);
            let dayClasses = css.dayButton;

            if (!isCurrentMonth) {
              dayClasses += ` ${css.muted}`;
            }
            if (isToday) {
              dayClasses += ` ${css.today}`;
            }
            if (isCursored) {
              dayClasses += ` ${css.cursor}`;
            }
            if (isSelected) {
              dayClasses += ` ${css.selected}`;
            }
            if (isExcluded) {
              dayClasses += ` ${css.excluded}`;
            }

            const titleDayString = day.format('dddd');
            const titleFormattedDay = day.format(this.props.dateFormat);

            return (

                <DayButton
                  onFocus={this.props.buttonFocus}
                  className={dayClasses}
                  title={formatMsg({ id: 'stripes-components.selectDay' }, { dayOfWeek: titleDayString, date: titleFormattedDay })}
                  onDayClick={this.handleDayClick}
                  day={day}
                  onKeyDown={this.props.onKeyDown}
                  onBlur={this.props.onBlur}
                  id={`datepicker-choose-date-button-${day.format('D')}-${id}`}
                >
                  {day.format('D')}
                </DayButton>

            );
          },
        );
        return
{days}
;
      },
    );

    let k = moment.localeData()._week.dow; // eslint-disable-line no-underscore-dangle
    const daysOfWeek = [];
    while (daysOfWeek.length < 7) {
      daysOfWeek.push({moment().day(k).format('dd')}	);
      k = k < 6 ? k += 1 : 0;
    }

    const arrowStyle = { padding: '4px' };

    return (
      <div className={css.calendar} id={`datepicker-calendar-container-${id}`} ref={(r) => { this._calendarContainer = r; }}>

                  <button id={`datepicker-previous-year-button-${id}`} className={css.nav} type="button" onClick={this.previousYear} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToPreviousYear' })}><Icon icon="left-double-chevron" /></button>

                  <button id={`datepicker-previous-month-button-${id}`} href="#" className={css.nav} type="button" onClick={this.previousMonth} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToPreviousMonth' })}><Icon icon="left-arrow" /></button>

                {moment().month(this.state.month).format('MMMM')} {this.state.year}

                  <button href="#" id={`datepicker-next-month-button-${id}`} className={css.nav} type="button" onClick={this.nextMonth} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToNextMonth' })} ><Icon icon="right-arrow" /></button>

                  <button href="#" id={`datepicker-next-year-button-${id}`} className={css.nav} type="button" onClick={this.nextYear} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToNextYear' })}><Icon icon="right-double-chevron" /></button>

                {daysOfWeek}

              {weeks}

    );
  }
}

Calendar.propTypes = propTypes;
Calendar.defaultProps = defaultProps;
Calendar.contextTypes = contextTypes;

export default Calendar;
<commit_msg>Calendar use getDerivedStateFromProps<commit_after>/*
* Display calendar UI for datepicker.
* Sync the cursor to the selected date or default to today.
* Month is rendered based on cursor date.
* Handles date math via moment.
*/

import React from 'react';
import { intlShape } from 'react-intl';
import PropTypes from 'prop-types';
import Moment from 'moment';
import { extendMoment } from 'moment-range';
import DayButton from './DayButton';
import Icon from '../Icon';

import css from './Calendar.css';

const moment = extendMoment(Moment);

function getCalendar(year, month) {
  const startDate = moment([year, month]);
  const firstDay = moment(startDate).startOf('month');
  const endDay = moment(startDate).endOf('month');
  const monthRange = moment.range(firstDay, endDay);

  const weeks = [];
  const calendar = [];
  const rowStartArray = [];
  const rowEndArray = [];

  const weekdays = Array.from(monthRange.by('days'));
  weekdays.forEach((mo) => {
    const ref = mo.week();
    if (weeks.indexOf(ref) < 0) {
      weeks.push(mo.week());
      const endClone = moment(mo);
      rowStartArray.push(mo.weekday(0));
      rowEndArray.push(endClone.weekday(6));
    }
  });

  for (let i = 0; i < weeks.length; i += 1) {
    const weekRange = moment.range(rowStartArray[i], rowEndArray[i]);
    calendar.push(weekRange);
  }

  return calendar;
}

const propTypes = {
  onSetDate: PropTypes.func,
  dateFormat: PropTypes.string,
  selectedDate: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
  locale: PropTypes.string,
  onBlur: PropTypes.func,
  onKeyDown: PropTypes.func,
  buttonFocus: PropTypes.func,
  id: PropTypes.string,
  excludeDates: PropTypes.oneOfType([
    PropTypes.array,
    PropTypes.object,
    PropTypes.string,
  ]),
};

const defaultProps = {
  dateFormat: 'MM DD YYYY',
  onSetDate: () => null,
  locale: 'en',
};

const contextTypes = {
  intl: intlShape
};

class Calendar extends React.Component {
  constructor(props) {
    super(props);

    moment.locale(this.props.locale);

    let inputDate; // initial date for state
    let cursorDate;
    if (this.props.selectedDate === '' || this.props.selectedDate === 'undefined') {
      inputDate = null;
      cursorDate = moment();
    } else if (moment(this.props.selectedDate, this.props.dateFormat, true).isValid()) {
      inputDate = moment(this.props.selectedDate, this.props.dateFormat, true);
      cursorDate = inputDate;
    } else {
      inputDate = moment();
      cursorDate = moment();
    }

    const base = moment(cursorDate);
    const month = base.month();
    const year = base.year();

    this.state = {
      cursorDate,
      date: inputDate,
      month,
      year,
      calendar: getCalendar(year, month),
    };

    this.handleDayClick = this.handleDayClick.bind(this);
    this.setDate = this.setDate.bind(this);
    this.nextMonth = this.nextMonth.bind(this);
    this.previousMonth = this.previousMonth.bind(this);
    this.nextYear = this.nextYear.bind(this);
    this.previousYear = this.previousYear.bind(this);
    this.checkExcluded = this.checkExcluded.bind(this);
    this.isDateSelected = this.isDateSelected.bind(this);
    this.updateCursorDate = this.updateCursorDate.bind(this);
    this.getDOMContainer = this.getDOMContainer.bind(this);
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    // When the selected date has changed, update the state with it
    let stateUpdate;
    if (nextProps.selectedDate !== prevState.selectedDate) {
      const moDate = moment(nextProps.selectedDate, prevState.dateFormat, true);
      if (moDate.isValid()) {
        if (moDate !== prevState.date) {
          // const moDate = moment(nextProps.selectedDate);
          const month = moDate.month();
          const year = moDate.year();
          stateUpdate = {
            cursorDate: moDate,
            date: moDate,
            month,
            year,
            calendar: getCalendar(year, month),
            selectedDate: nextProps.selectedDate,
            dateFormat: nextProps.dateFormat,
          };
        }
      } else { // fix navigation issue on null or invalid date
        const fallbackDate = moment();
        const month = fallbackDate.month();
        const year = fallbackDate.year();
        stateUpdate = {
          date: fallbackDate,
          month,
          year,
          calendar: getCalendar(year, month),
          selectedDate: nextProps.selectedDate,
          dateFormat: nextProps.dateFormat,
        };
      }
    }

    if (stateUpdate) {
      const newState = prevState;
      Object.assign(newState, stateUpdate);
      newState.calendar = getCalendar(newState.year, newState.month);
      return newState;
    } else {
      return null
    }
  }

  setDate(day) {
    this.setState({
      date: day,
    });
  }

  getDOMContainer() {
    return this._calendarContainer;
  }

  getMonth() {
    return this.state.month;
  }

  getCursorDate() {
    return this.state.cursorDate;
  }

  previousYear(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'y');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextYear(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'y');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousMonth(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'M');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextMonth(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'M');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextDay(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(1, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousDay(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(1, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  nextWeek(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.add(7, 'd');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  previousWeek(e) {
    if (e) { e.preventDefault(); }
    const curDate = this.state.cursorDate;
    curDate.subtract(7, 'days');
    this.updateCursorDate(curDate);
    return moment(curDate).format(this.props.dateFormat);
  }

  updateCursorDate(date) {
    this.setState((oldState) => {
      const newState = Object.assign({}, oldState);
      newState.cursorDate = date;
      newState.month = newState.cursorDate.month();
      newState.year = newState.cursorDate.year();
      if (newState.year !== oldState.year || newState.month !== oldState.month) {
        newState.calendar = getCalendar(newState.year, newState.month);
      }
      return newState;
    });
  }

  handleDayClick(day, e) {
    if (e) { e.preventDefault(); }
    this.setDate(day);
    this.props.onSetDate(e, day, day.format(this.props.dateFormat));
  }

  checkExcluded(date) {
    let excluded = false;
    if (!this.props.excludeDates) {
      return false;
    }

    const ex = this.props.excludeDates;
    switch (typeof (ex)) {
      case 'string':
        if (date.isSame(moment(ex), 'day')) {
          return true;
        }
        break;
      case 'object':
        if (moment.isMoment(ex)) {
          if (date.isSame(ex, 'day')) {
            return true;
          }
        } else { // excluded is an array...
          ex.forEach((exd) => {
            if (moment.isMoment(exd)) {
              if (date.isSame(exd, 'day')) {
                excluded = true;
              }
            } else if (date.isSame(moment(exd), 'day')) {
              excluded = true;
            }
          });
        }
        break;
      default:
        return false;
    }
    return excluded;
  }

  isDateSelected(day) {
    if (this.props.selectedDate) {
      return day.format(this.props.dateFormat) ===
        moment(this.props.selectedDate, this.props.dateFormat, true).format(this.props.dateFormat);
    }
    if (this.state.date) {
      return day.format(this.props.dateFormat) ===
        moment(this.state.date, this.props.dateFormat, true).format(this.props.dateFormat);
    }
    return false;
  }

  render() {
    const { id } = this.props;
    const formatMsg = this.context.intl.formatMessage;
    let weekCount = 0;
    const weeks = this.state.calendar.map(
      (week) => {
        weekCount += 1;
        const dayList = [];
        const weekDays = Array.from(week.by('days'));
        weekDays.forEach((day) => { dayList.push(day); });

        const days = dayList.map(
          (day) => {
            const isCurrentMonth = day.month() === this.state.month;
            const isToday = day.isSame(moment(), 'day');
            const isSelected = this.isDateSelected(day);
            const isCursored = day.isSame(this.state.cursorDate, 'day');
            const isExcluded = this.checkExcluded(day);
            let dayClasses = css.dayButton;

            if (!isCurrentMonth) {
              dayClasses += ` ${css.muted}`;
            }
            if (isToday) {
              dayClasses += ` ${css.today}`;
            }
            if (isCursored) {
              dayClasses += ` ${css.cursor}`;
            }
            if (isSelected) {
              dayClasses += ` ${css.selected}`;
            }
            if (isExcluded) {
              dayClasses += ` ${css.excluded}`;
            }

            const titleDayString = day.format('dddd');
            const titleFormattedDay = day.format(this.props.dateFormat);

            return (

                <DayButton
                  onFocus={this.props.buttonFocus}
                  className={dayClasses}
                  title={formatMsg({ id: 'stripes-components.selectDay' }, { dayOfWeek: titleDayString, date: titleFormattedDay })}
                  onDayClick={this.handleDayClick}
                  day={day}
                  onKeyDown={this.props.onKeyDown}
                  onBlur={this.props.onBlur}
                  id={`datepicker-choose-date-button-${day.format('D')}-${id}`}
                >
                  {day.format('D')}
                </DayButton>

            );
          },
        );
        return
{days}
;
      },
    );

    let k = moment.localeData()._week.dow; // eslint-disable-line no-underscore-dangle
    const daysOfWeek = [];
    while (daysOfWeek.length < 7) {
      daysOfWeek.push({moment().day(k).format('dd')}	);
      k = k < 6 ? k += 1 : 0;
    }

    const arrowStyle = { padding: '4px' };

    return (
      <div className={css.calendar} id={`datepicker-calendar-container-${id}`} ref={(r) => { this._calendarContainer = r; }}>

                  <button id={`datepicker-previous-year-button-${id}`} className={css.nav} type="button" onClick={this.previousYear} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToPreviousYear' })}><Icon icon="left-double-chevron" /></button>

                  <button id={`datepicker-previous-month-button-${id}`} href="#" className={css.nav} type="button" onClick={this.previousMonth} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToPreviousMonth' })}><Icon icon="left-arrow" /></button>

                {moment().month(this.state.month).format('MMMM')} {this.state.year}

                  <button href="#" id={`datepicker-next-month-button-${id}`} className={css.nav} type="button" onClick={this.nextMonth} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToNextMonth' })} ><Icon icon="right-arrow" /></button>

                  <button href="#" id={`datepicker-next-year-button-${id}`} className={css.nav} type="button" onClick={this.nextYear} onKeyDown={this.props.onKeyDown} tabIndex="-1" style={arrowStyle} title={formatMsg({ id: 'stripes-components.goToNextYear' })}><Icon icon="right-double-chevron" /></button>

                {daysOfWeek}

              {weeks}

    );
  }
}

Calendar.propTypes = propTypes;
Calendar.defaultProps = defaultProps;
Calendar.contextTypes = contextTypes;

export default Calendar;
<|endoftext|>


<commit_before>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    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 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
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const Sensor = require('./Sensor.js').Sensor;
const exec = require('child-process-promise').exec;
const rclient = require('../util/redis_manager.js').getRedisClient();
const f = require('../net2/Firewalla.js');
const LOG_PREFIX = "[FW_ACL_AUDIT]";
const SECLOG_PREFIX = "[FW_SEC_AUDIT]";
const {Address4, Address6} = require('ip-address');
const sysManager = require('../net2/SysManager.js');
const HostTool = require('../net2/HostTool.js');
const hostTool = new HostTool();
const HostManager = require('../net2/HostManager')
const hostManager = new HostManager();
const networkProfileManager = require('../net2/NetworkProfileManager')
const vpnProfileManager = require('../net2/VPNProfileManager.js');
const DNSTool = require('../net2/DNSTool.js');
const dnsTool = new DNSTool();
const Message = require('../net2/Message.js');
const sem = require('./SensorEventManager.js').getInstance();
const timeSeries = require("../util/TimeSeries.js").getTimeSeries()
const Constants = require('../net2/Constants.js');

const os = require('os')
const Tail = require('../vendor_lib/always-tail.js');

const _ = require('lodash')

const auditLogFile = "/alog/acl-audit.log";

class ACLAuditLogPlugin extends Sensor {
  constructor() {
    super()

    this.featureName = "acl_audit";
    this.startTime = (Date.now() - os.uptime()*1000) / 1000
    this.buffer = { }
    this.bufferTs = Date.now() / 1000
  }

  async run() {
    this.hookFeature();
    this.auditLogReader = null;
    this.aggregator = null
  }

  async job() {
    super.job()
    this.auditLogReader = new Tail(auditLogFile, '\n');
    if (this.auditLogReader != null) {
      this.auditLogReader.on('line', line => {
        this._processIptablesLog(line)
          .catch(err => log.error('Failed to process log', err, line))
      });
      this.auditLogReader.on('error', (err) => {
        log.error("Error while reading acl audit log", err.message);
      })
    }

    sem.on(Message.MSG_ACL_DNS, message => {
      if (message && message.record)
        this._processDnsRecord(message.record)
          .catch(err => log.error('Failed to process record', err, message.record))
    });
  }

  getDescriptor(r) {
    return r.type == 'dns' ?
      `dns:${r.dn}:${r.qc}:${r.qt}:${r.rc}:${r.qt}` :
      `ip:${r.fd == 'out' ? r.sh : r.dh}:${r.dp}:${r.fd}`
  }

  writeBuffer(mac, record) {
    if (!this.buffer[mac]) this.buffer[mac] = {}
    const descriptor = this.getDescriptor(record)
    if (this.buffer[mac][descriptor]) {
      const s = this.buffer[mac][descriptor]
      // _.min() and _.max() will ignore non-number values
      s.ts = _.min([s.ts, record.ts])
      s.ets = _.max([s.ts, s.ets, record.ts, record.ets])
      s.ct += record.ct
      if (s.sp) s.sp = _.uniq(s.sp, record.sp)
    } else {
      this.buffer[mac][descriptor] = record
    }
  }

  // Jul  2 16:35:57 firewalla kernel: [ 6780.606787] [FW_ACL_AUDIT]IN=br0 OUT=eth0 PHYSIN=eth1.999 MAC=20:6d:31:fe:00:07:88:e9:fe:86:ff:94:08:00 SRC=192.168.210.191 DST=23.129.64.214 LEN=64 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=63349 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 MARK=0x87
  // THIS MIGHT BE A BUG: The calculated timestamp seems to always have a few seconds gap with real event time, but the gap is constant. The readable time seem to be accurate, but precision is not enough for event order distinguishing
  async _processIptablesLog(line) {
    // log.debug(line)
    const uptime = Number(line.match(/\[\s*([\d.]+)\]/)[1])
    const ts = Math.round((this.startTime + uptime) * 1000) / 1000;
    const secTagIndex = line.indexOf(SECLOG_PREFIX)
    const security = secTagIndex > 0
    // extract content after log prefix
    const content = line.substring(security ?
      secTagIndex + SECLOG_PREFIX.length : line.indexOf(LOG_PREFIX) + LOG_PREFIX.length
    );
    if (!content || content.length == 0)
      return;
    const params = content.split(' ');
    const record = { ts, type: 'ip', ct: 1};
    if (security) record.sec = 1
    let mac, srcMac, dstMac, intf, localIP, remoteIP
    for (const param of params) {
      const kvPair = param.split('=');
      if (kvPair.length !== 2)
        continue;
      const k = kvPair[0];
      const v = kvPair[1];
      switch (k) {
        case "SRC": {
          record.sh = v;
          break;
        }
        case "DST": {
          record.dh = v;
          break;
        }
        case "PROTO": {
          record.pr = v.toLowerCase();
          // ignore icmp packets
          if (record.pr == 'icmp') return
          break;
        }
        case "SPT": {
          record.sp = [ Number(v) ];
          break;
        }
        case "DPT": {
          record.dp = Number(v);
          break;
        }
        case 'MAC': {
          dstMac = v.substring(0, 17).toUpperCase()
          srcMac = v.substring(18, 35).toUpperCase()
          break;
        }
        case 'IN': {
          // always use IN for interface
          // when traffic coming from external network, it'll hit WAN interface and that's what we want
          intf = sysManager.getInterface(v)
          record.intf = intf.uuid
          break;
        }
        default:
      }
    }

    if (sysManager.isMulticastIP(record.dh, intf.name, false)) return

    // check direction, keep it same as flow.fd
    // in, initiated from inside
    // out, initated from outside
    const wanIPs = sysManager.myWanIps()
    const srcIsLocal = sysManager.isLocalIP(record.sh) || wanIPs.v4.includes(record.sh) || wanIPs.v6.includes(record.sh)
    const dstIsLocal = sysManager.isLocalIP(record.dh) || wanIPs.v4.includes(record.dh) || wanIPs.v6.includes(record.dh)

    if (srcIsLocal) {
      mac = srcMac;
      localIP = record.sh
      remoteIP = record.dh

      if (dstIsLocal)
        record.fd = 'lo';
      else
        record.fd = 'in';
    } else if (dstIsLocal) {
      record.fd = 'out';
      mac = dstMac;
      localIP = record.dh
      remoteIP = record.sh
    } else {
      return
    }

    // broadcast mac address
    if (mac == 'FF:FF:FF:FF:FF:FF') return

    // local IP being Firewalla's own interface, use if:<uuid> as "mac"
    if (new Address4(localIP).isValid() ? sysManager.isMyIP(localIP, false) : sysManager.isMyIP6(localIP, false)) {
      log.debug(line)
      mac = `${Constants.NS_INTERFACE}:${intf.uuid}`
    }

    if (intf.name === "tun_fwvpn") {
      const vpnProfile = vpnProfileManager.getProfileCNByVirtualAddr(localIP);
      if (!vpnProfile) throw new Error('VPNProfile not found for', localIP);
      mac = `${Constants.NS_VPN_PROFILE}:${vpnProfile}`;
      record.rl = vpnProfileManager.getRealAddrByVirtualAddr(localIP);
    }

    // TODO: is dns resolution necessary here?
    const domain = await dnsTool.getDns(remoteIP);
    if (domain)
      record.dn = domain;

    this.writeBuffer(mac, record)
  }

  async _processDnsRecord(record) {
    record.type = 'dns'
    record.pr = 'dns'

    const intf = new Address4(record.sh).isValid() ?
      sysManager.getInterfaceViaIP4(record.sh, false) :
      sysManager.getInterfaceViaIP6(record.sh, false)

    if (!intf) {
      log.debug('Interface not found for', record.sh);
      return null
    }

    record.intf = intf.uuid

    let mac
    if (intf.name === "tun_fwvpn") {
      const vpnProfile = vpnProfileManager.getProfileCNByVirtualAddr(record.sh);
      if (!vpnProfile) throw new Error('VPNProfile not found for', record.sh);
      mac = `${Constants.NS_VPN_PROFILE}:${vpnProfile}`;
      record.rl = vpnProfileManager.getRealAddrByVirtualAddr(record.sh);
    } else {
      mac = await hostTool.getMacByIPWithCache(record.sh, false);
    }

    if (!mac) {
      log.debug('MAC address not found for', record.sh)
      return
    }

    record.ct = 1;

    this.writeBuffer(mac, record)
  }

  _getAuditKey(mac, block = true) {
    return block ? `audit:drop:${mac}` : `audit:accept:${mac}`;
  }

  async writeLogs() {
    try {
      log.debug('Start writing logs', this.bufferTs)
      // log.debug(JSON.stringify(this.buffer))

      const buffer = this.buffer
      this.buffer = { }
      log.debug(buffer)

      for (const mac in buffer) {
        for (const descriptor in buffer[mac]) {
          const record = buffer[mac][descriptor];
          const {type, ts, ets, ct, intf} = record
          const _ts = ets || ts
          const block = type == 'dns' ?
            record.rc == 3 /*NXDOMAIN*/ &&
            (record.qt == 1 /*A*/ || record.qt == 28 /*AAAA*/ ) &&
            record.dp == 53
            :
            true
          const tags = []
          if (
            !mac.startsWith(Constants.NS_VPN_PROFILE + ':') &&
            !mac.startsWith(Constants.NS_INTERFACE + ':')
          ) {
            const host = hostManager.getHostFastByMAC(mac);
            if (host) tags.push(...await host.getTags())
          }
          const networkProfile = networkProfileManager.getNetworkProfile(intf);
          if (networkProfile) tags.push(...networkProfile.getTags());
          record.tags = _.uniq(tags)

          const key = this._getAuditKey(mac, block)
          await rclient.zaddAsync(key, _ts, JSON.stringify(record));

          const expires = this.config.expires || 86400
          await rclient.expireatAsync(key, parseInt(new Date / 1000) + expires)

          const hitType = type + (block ? 'B' : '')
          timeSeries.recordHit(`${hitType}`, _ts, ct)
          timeSeries.recordHit(`${hitType}:${mac}`, _ts, ct)
          timeSeries.recordHit(`${hitType}:intf:${intf}`, _ts, ct)
          for (const tag of record.tags) {
            timeSeries.recordHit(`${hitType}:tag:${tag}`, _ts, ct)
          }
        }
      }
      timeSeries.exec()
    } catch(err) {
      log.error("Failed to write audit logs", err)
    }
  }

  // Works similar to flowStash in BroDetect, reduce memory is the main purpose here
  async mergeLogs(startOpt, endOpt) {
    try {
      const end = endOpt || Math.floor(new Date() / 1000 / this.config.interval) * this.config.interval
      const start = startOpt || end - this.config.interval
      log.debug('Start merging', start, end)

      for (const block of [true, false]) {
        const auditKeys = await rclient.scanResults(this._getAuditKey('*', block), 1000)
        log.debug('Key(mac) count: ', auditKeys.length)
        for (const key of auditKeys) {
          const records = await rclient.zrangebyscoreAsync(key, start, end)
          // const mac = key.substring(11) // audit:drop:<mac>

          const stash = {}
          for (const recordString of records) {
            try {
              const record = JSON.parse(recordString)
              const descriptor = this.getDescriptor(record)

              if (stash[descriptor]) {
                const s = stash[descriptor]
                // _.min() and _.max() will ignore non-number values
                s.ts = _.min([s.ts, record.ts])
                s.ets = _.max([s.ts, s.ets, record.ts, record.ets])
                s.ct += record.ct
                if (s.sp) s.sp = _.uniq(s.sp, record.sp)
              } else {
                stash[descriptor] = record
              }
            } catch(err) {
              log.error('Failed to process record', err, recordString)
            }
          }

          const transaction = [];
          transaction.push(['zremrangebyscore', key, start, end]);
          for (const descriptor in stash) {
            const record = stash[descriptor]
            transaction.push(['zadd', key, record.ets || record.ts, JSON.stringify(record)])
          }
          const expires = this.config.expires || 86400
          await rclient.expireatAsync(key, parseInt(new Date / 1000) + expires)
          transaction.push(['expireat', key, parseInt(new Date / 1000) + this.config.expires])

          // catch this to proceed onto the next iteration
          try {
            log.debug(transaction)
            await rclient.multi(transaction).execAsync();
            log.debug("Audit:Save:Removed", key);
          } catch (err) {
            log.error("Audit:Save:Error", err);
          }
        }
      }

    } catch(err) {
      log.error("Failed to merge audit logs", err)
    }
  }

  async globalOn() {
    super.globalOn()

    await exec(`${f.getFirewallaHome()}/scripts/audit-run`)

    this.bufferDumper = this.bufferDumper || setInterval(this.writeLogs.bind(this), (this.config.buffer || 30) * 1000)
    this.aggregator = this.aggregator || setInterval(this.mergeLogs.bind(this), (this.config.interval || 300) * 1000)
  }

  async globalOff() {
    super.globalOn()

    await exec(`${f.getFirewallaHome()}/scripts/audit-stop`)

    clearInterval(this.bufferDumper)
    clearInterval(this.aggregator)
    this.bufferDumper = this.aggregator = undefined
  }

}

module.exports = ACLAuditLogPlugin;
<commit_msg>fix some cases that fails to find mac address<commit_after>/*    Copyright 2016-2021 Firewalla Inc.
 *
 *    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 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
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const Sensor = require('./Sensor.js').Sensor;
const exec = require('child-process-promise').exec;
const rclient = require('../util/redis_manager.js').getRedisClient();
const f = require('../net2/Firewalla.js');
const LOG_PREFIX = "[FW_ACL_AUDIT]";
const SECLOG_PREFIX = "[FW_SEC_AUDIT]";
const {Address4, Address6} = require('ip-address');
const sysManager = require('../net2/SysManager.js');
const HostTool = require('../net2/HostTool.js');
const hostTool = new HostTool();
const HostManager = require('../net2/HostManager')
const hostManager = new HostManager();
const networkProfileManager = require('../net2/NetworkProfileManager')
const vpnProfileManager = require('../net2/VPNProfileManager.js');
const DNSTool = require('../net2/DNSTool.js');
const dnsTool = new DNSTool();
const Message = require('../net2/Message.js');
const sem = require('./SensorEventManager.js').getInstance();
const timeSeries = require("../util/TimeSeries.js").getTimeSeries()
const Constants = require('../net2/Constants.js');
const l2 = require('../util/Layer2.js');

const os = require('os')
const Tail = require('../vendor_lib/always-tail.js');

const _ = require('lodash')

const auditLogFile = "/alog/acl-audit.log";

class ACLAuditLogPlugin extends Sensor {
  constructor() {
    super()

    this.featureName = "acl_audit";
    this.startTime = (Date.now() - os.uptime()*1000) / 1000
    this.buffer = { }
    this.bufferTs = Date.now() / 1000
  }

  async run() {
    this.hookFeature();
    this.auditLogReader = null;
    this.aggregator = null
  }

  async job() {
    super.job()
    this.auditLogReader = new Tail(auditLogFile, '\n');
    if (this.auditLogReader != null) {
      this.auditLogReader.on('line', line => {
        this._processIptablesLog(line)
          .catch(err => log.error('Failed to process log', err, line))
      });
      this.auditLogReader.on('error', (err) => {
        log.error("Error while reading acl audit log", err.message);
      })
    }

    sem.on(Message.MSG_ACL_DNS, message => {
      if (message && message.record)
        this._processDnsRecord(message.record)
          .catch(err => log.error('Failed to process record', err, message.record))
    });
  }

  getDescriptor(r) {
    return r.type == 'dns' ?
      `dns:${r.dn}:${r.qc}:${r.qt}:${r.rc}:${r.qt}` :
      `ip:${r.fd == 'out' ? r.sh : r.dh}:${r.dp}:${r.fd}`
  }

  writeBuffer(mac, record) {
    if (!this.buffer[mac]) this.buffer[mac] = {}
    const descriptor = this.getDescriptor(record)
    if (this.buffer[mac][descriptor]) {
      const s = this.buffer[mac][descriptor]
      // _.min() and _.max() will ignore non-number values
      s.ts = _.min([s.ts, record.ts])
      s.ets = _.max([s.ts, s.ets, record.ts, record.ets])
      s.ct += record.ct
      if (s.sp) s.sp = _.uniq(s.sp, record.sp)
    } else {
      this.buffer[mac][descriptor] = record
    }
  }

  // Jul  2 16:35:57 firewalla kernel: [ 6780.606787] [FW_ACL_AUDIT]IN=br0 OUT=eth0 PHYSIN=eth1.999 MAC=20:6d:31:fe:00:07:88:e9:fe:86:ff:94:08:00 SRC=192.168.210.191 DST=23.129.64.214 LEN=64 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=63349 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 MARK=0x87
  // THIS MIGHT BE A BUG: The calculated timestamp seems to always have a few seconds gap with real event time, but the gap is constant. The readable time seem to be accurate, but precision is not enough for event order distinguishing
  async _processIptablesLog(line) {
    // log.debug(line)
    const uptime = Number(line.match(/\[\s*([\d.]+)\]/)[1])
    const ts = Math.round((this.startTime + uptime) * 1000) / 1000;
    const secTagIndex = line.indexOf(SECLOG_PREFIX)
    const security = secTagIndex > 0
    // extract content after log prefix
    const content = line.substring(security ?
      secTagIndex + SECLOG_PREFIX.length : line.indexOf(LOG_PREFIX) + LOG_PREFIX.length
    );
    if (!content || content.length == 0)
      return;
    const params = content.split(' ');
    const record = { ts, type: 'ip', ct: 1};
    if (security) record.sec = 1
    let mac, srcMac, dstMac, intf, localIP, remoteIP
    for (const param of params) {
      const kvPair = param.split('=');
      if (kvPair.length !== 2)
        continue;
      const k = kvPair[0];
      const v = kvPair[1];
      switch (k) {
        case "SRC": {
          record.sh = v;
          break;
        }
        case "DST": {
          record.dh = v;
          break;
        }
        case "PROTO": {
          record.pr = v.toLowerCase();
          // ignore icmp packets
          if (record.pr == 'icmp') return
          break;
        }
        case "SPT": {
          record.sp = [ Number(v) ];
          break;
        }
        case "DPT": {
          record.dp = Number(v);
          break;
        }
        case 'MAC': {
          dstMac = v.substring(0, 17).toUpperCase()
          srcMac = v.substring(18, 35).toUpperCase()
          break;
        }
        case 'IN': {
          // always use IN for interface
          // when traffic coming from external network, it'll hit WAN interface and that's what we want
          intf = sysManager.getInterface(v)
          record.intf = intf.uuid
          break;
        }
        default:
      }
    }

    if (sysManager.isMulticastIP(record.dh, intf.name, false)) return

    // check direction, keep it same as flow.fd
    // in, initiated from inside
    // out, initated from outside
    const wanIPs = sysManager.myWanIps()
    const srcIsLocal = sysManager.isLocalIP(record.sh) || wanIPs.v4.includes(record.sh) || wanIPs.v6.includes(record.sh)
    const dstIsLocal = sysManager.isLocalIP(record.dh) || wanIPs.v4.includes(record.dh) || wanIPs.v6.includes(record.dh)

    if (srcIsLocal) {
      mac = srcMac;
      localIP = record.sh
      remoteIP = record.dh

      if (dstIsLocal)
        record.fd = 'lo';
      else
        record.fd = 'in';
    } else if (dstIsLocal) {
      record.fd = 'out';
      mac = dstMac;
      localIP = record.dh
      remoteIP = record.sh
    } else {
      return
    }

    // broadcast mac address
    if (mac == 'FF:FF:FF:FF:FF:FF') return

    if (intf.name === "tun_fwvpn") {
      const vpnProfile = vpnProfileManager.getProfileCNByVirtualAddr(localIP);
      if (!vpnProfile) {
        log.WARN('VPNProfile not found for', localIP);
        mac = `${Constants.NS_INTERFACE}:${intf.uuid}`
      } else {
        mac = `${Constants.NS_VPN_PROFILE}:${vpnProfile}`;
        record.rl = vpnProfileManager.getRealAddrByVirtualAddr(localIP);
      }
    }
    // local IP being Firewalla's own interface, use if:<uuid> as "mac"
    else if (new Address4(localIP).isValid() ?
      sysManager.isMyIP(localIP, false) :
      sysManager.isMyIP6(localIP, false)
    ) {
      mac = `${Constants.NS_INTERFACE}:${intf.uuid}`
    }
    // try resolve ip to device mac
    else if (!mac || mac == intf.mac_address.toUpperCase()) {
      mac = await l2.getMACAsync(localIP).catch(err => {
        log.warn("Failed to get MAC address from link layer for " + localIP, err);
        mac = `${Constants.NS_INTERFACE}:${intf.uuid}`
      })
    }
    // mac != intf.mac_address => mac is device mac, keep mac unchanged

    // TODO: is dns resolution necessary here?
    const domain = await dnsTool.getDns(remoteIP);
    if (domain)
      record.dn = domain;

    this.writeBuffer(mac, record)
  }

  async _processDnsRecord(record) {
    record.type = 'dns'
    record.pr = 'dns'

    const intf = new Address4(record.sh).isValid() ?
      sysManager.getInterfaceViaIP4(record.sh, false) :
      sysManager.getInterfaceViaIP6(record.sh, false)

    if (!intf) {
      log.debug('Interface not found for', record.sh);
      return null
    }

    record.intf = intf.uuid

    let mac
    if (intf.name === "tun_fwvpn") {
      const vpnProfile = vpnProfileManager.getProfileCNByVirtualAddr(record.sh);
      if (!vpnProfile) throw new Error('VPNProfile not found for', record.sh);
      mac = `${Constants.NS_VPN_PROFILE}:${vpnProfile}`;
      record.rl = vpnProfileManager.getRealAddrByVirtualAddr(record.sh);
    } else {
      mac = await hostTool.getMacByIPWithCache(record.sh, false);
    }

    if (!mac) {
      log.debug('MAC address not found for', record.sh)
      return
    }

    record.ct = 1;

    this.writeBuffer(mac, record)
  }

  _getAuditKey(mac, block = true) {
    return block ? `audit:drop:${mac}` : `audit:accept:${mac}`;
  }

  async writeLogs() {
    try {
      log.debug('Start writing logs', this.bufferTs)
      // log.debug(JSON.stringify(this.buffer))

      const buffer = this.buffer
      this.buffer = { }
      log.debug(buffer)

      for (const mac in buffer) {
        for (const descriptor in buffer[mac]) {
          const record = buffer[mac][descriptor];
          const {type, ts, ets, ct, intf} = record
          const _ts = ets || ts
          const block = type == 'dns' ?
            record.rc == 3 /*NXDOMAIN*/ &&
            (record.qt == 1 /*A*/ || record.qt == 28 /*AAAA*/ ) &&
            record.dp == 53
            :
            true
          const tags = []
          if (
            !mac.startsWith(Constants.NS_VPN_PROFILE + ':') &&
            !mac.startsWith(Constants.NS_INTERFACE + ':')
          ) {
            const host = hostManager.getHostFastByMAC(mac);
            if (host) tags.push(...await host.getTags())
          }
          const networkProfile = networkProfileManager.getNetworkProfile(intf);
          if (networkProfile) tags.push(...networkProfile.getTags());
          record.tags = _.uniq(tags)

          const key = this._getAuditKey(mac, block)
          await rclient.zaddAsync(key, _ts, JSON.stringify(record));

          const expires = this.config.expires || 86400
          await rclient.expireatAsync(key, parseInt(new Date / 1000) + expires)

          const hitType = type + (block ? 'B' : '')
          timeSeries.recordHit(`${hitType}`, _ts, ct)
          timeSeries.recordHit(`${hitType}:${mac}`, _ts, ct)
          timeSeries.recordHit(`${hitType}:intf:${intf}`, _ts, ct)
          for (const tag of record.tags) {
            timeSeries.recordHit(`${hitType}:tag:${tag}`, _ts, ct)
          }
        }
      }
      timeSeries.exec()
    } catch(err) {
      log.error("Failed to write audit logs", err)
    }
  }

  // Works similar to flowStash in BroDetect, reduce memory is the main purpose here
  async mergeLogs(startOpt, endOpt) {
    try {
      const end = endOpt || Math.floor(new Date() / 1000 / this.config.interval) * this.config.interval
      const start = startOpt || end - this.config.interval
      log.debug('Start merging', start, end)

      for (const block of [true, false]) {
        const auditKeys = await rclient.scanResults(this._getAuditKey('*', block), 1000)
        log.debug('Key(mac) count: ', auditKeys.length)
        for (const key of auditKeys) {
          const records = await rclient.zrangebyscoreAsync(key, start, end)
          // const mac = key.substring(11) // audit:drop:<mac>

          const stash = {}
          for (const recordString of records) {
            try {
              const record = JSON.parse(recordString)
              const descriptor = this.getDescriptor(record)

              if (stash[descriptor]) {
                const s = stash[descriptor]
                // _.min() and _.max() will ignore non-number values
                s.ts = _.min([s.ts, record.ts])
                s.ets = _.max([s.ts, s.ets, record.ts, record.ets])
                s.ct += record.ct
                if (s.sp) s.sp = _.uniq(s.sp, record.sp)
              } else {
                stash[descriptor] = record
              }
            } catch(err) {
              log.error('Failed to process record', err, recordString)
            }
          }

          const transaction = [];
          transaction.push(['zremrangebyscore', key, start, end]);
          for (const descriptor in stash) {
            const record = stash[descriptor]
            transaction.push(['zadd', key, record.ets || record.ts, JSON.stringify(record)])
          }
          const expires = this.config.expires || 86400
          await rclient.expireatAsync(key, parseInt(new Date / 1000) + expires)
          transaction.push(['expireat', key, parseInt(new Date / 1000) + this.config.expires])

          // catch this to proceed onto the next iteration
          try {
            log.debug(transaction)
            await rclient.multi(transaction).execAsync();
            log.debug("Audit:Save:Removed", key);
          } catch (err) {
            log.error("Audit:Save:Error", err);
          }
        }
      }

    } catch(err) {
      log.error("Failed to merge audit logs", err)
    }
  }

  async globalOn() {
    super.globalOn()

    await exec(`${f.getFirewallaHome()}/scripts/audit-run`)

    this.bufferDumper = this.bufferDumper || setInterval(this.writeLogs.bind(this), (this.config.buffer || 30) * 1000)
    this.aggregator = this.aggregator || setInterval(this.mergeLogs.bind(this), (this.config.interval || 300) * 1000)
  }

  async globalOff() {
    super.globalOn()

    await exec(`${f.getFirewallaHome()}/scripts/audit-stop`)

    clearInterval(this.bufferDumper)
    clearInterval(this.aggregator)
    this.bufferDumper = this.aggregator = undefined
  }

}

module.exports = ACLAuditLogPlugin;
<|endoftext|>


<commit_before>var TypeBetter = TypeBetter || {};

TypeBetter = {

  input: '',
  newValue: '',
  deletions: [],
  deletedChars: {},

  init: function(input) {
    // Set input field
    this.input = input;
    // Add events
    this.bindUIActions();
  },

  bindUIActions: function() {
    // Check every input
    if (this.input.addEventListener) {
      var actions = ['propertychange', 'change', 'input', 'paste'];

      for (var i = 0, j = actions.length; i < j; i++) {
        this.input.addEventListener(actions[i], this.handleQuotes, false);
      }
    }
    // Check for deletions
    this.input.onkeydown = function(event) {
      var key = event.keyCode || event.charCode;
      if ( key == 8 || key == 46 ) {
        TypeBetter.handleDeletions();
      }
    };
  },

  handleDeletions: function() {
    var deletedSpot = TypeBetter.input.selectionStart - 1,
        deletedChar = TypeBetter.newValue[deletedSpot];
    switch (deletedChar) {
      case '\u201c':
      case '\u201d':
      case '\u2018':
      case '\u2019':
      case '\u2032':
      case '\u2033':
      case '\u2034':
        if (TypeBetter.deletions.indexOf(deletedSpot) <= -1) {
          TypeBetter.deletions.push(deletedSpot);
        }
        return true;
        break;
      default:
        return false;
    }
  },

  handleQuotes: function() {
    // get current cursor position
    var start     = this.selectionStart,
        end       = this.selectionEnd,
        tempValue = TypeBetter.input.value;
    // Create array of deleted characters
    if (TypeBetter.deletions.length > 0) {
      // Loop through position of deleted characters
      for (i = 0; i < TypeBetter.deletions.length; i++) {
        var tempPosition = TypeBetter.deletions[i];
        // Add chars to array
        TypeBetter.deletedChars[i] = {"tempPosition": tempPosition, "tempChar": tempValue[tempPosition]};
        // Remove characters from input string
        tempValue = tempValue.substring(0, tempPosition) + tempValue.substring(tempPosition + 1, tempValue.length);
      }
    }
    // Replaces input field type with better type
    TypeBetter.newValue = tempValue
      // beginning "
      .replace(/(\W|^)"/g, '$1\u201c')
      // ending "
      .replace(/(\u201c|"[^"]*)([^"]*)(\u201c|"[^"]*)/g, '$1$2\u201d')
      // beginning '
      .replace(/(\W|^)'(\S)/g, '$1\u2018$2')
      .replace(/(\W|^)\u2032(\S)/g,'$1\u2018$2')
      .replace(/(\W|^)\u2019(\S)/g,'$1\u2018$2')
      // conjunction's possession
      .replace(/([a-z])'([a-z])/ig, '$1\u2019$2')
      // ending '
      .replace(/((\u2018[^']*)|[a-z])'([^0-9]|$)/ig, '$1\u2019$3')
      // backwards apostrophe
      .replace(/(\B|^)\u2018(?=([^\u2019]*\u2019\b)*([^\u2019\u2018]*\W[\u2019\u2018]\b|[^\u2019\u2018]*$))/ig, '$1\u2019')
      // abbreviated years like '82
      .replace(/(\u2019|')([0-9]{2}[^\u2019]*)(\u2018([^0-9]|$)|$|\u2019[a-z])/ig, '\u2019$2$3')
      // triple prime
      .replace(/'''/g, '\u2034')
      // double prime
      .replace(/("|'')/g, '\u2033')
      // prime
      .replace(/'/g, '\u2032')
      // hack for prime end quote
      .replace(/(\u2032|′)("|”|“)/g, '′”')
      // hack for apostrophe end quote
      .replace(/(\u2019|\u2018)("|”|“)/g, '’”')
      // ellipses
      .replace(/(\.\.\.)/, '\u2026')
      // mdash
      .replace(/\u2013\u2013|--/g, ' \u2014 ')
      // rock 'n' roll
      .replace(/(\s)(\u2018|')(n)(\u2019|')(\s)/gi, ' ’n’ ');
    if (TypeBetter.deletions.length > 0) {
      // Update deleted character positions

      // Loop through deleted characters
      for (var key in TypeBetter.deletedChars) {
        var deletions = TypeBetter.deletedChars[key];
        // Readd deleted characters
        TypeBetter.newValue = [TypeBetter.newValue.slice(0, deletions.tempPosition), deletions.tempChar, TypeBetter.newValue.slice(deletions.tempPosition)].join('');
      }
    }
    // Replace headline
    TypeBetter.input.value = TypeBetter.newValue;
    // Restore cursor position
    this.setSelectionRange(start, end);
  }

};
<commit_msg>Moving positions of deleted characters, still super buggy when there are a lot of changes to a headline<commit_after>var TypeBetter = TypeBetter || {};

TypeBetter = {

  input: '',
  newValue: '',
  deletions: [],
  deletedChars: {},

  init: function(input) {
    // Set input field
    this.input = input;
    // Add events
    this.bindUIActions();
  },

  bindUIActions: function() {
    // Check every input
    if (this.input.addEventListener) {
      var actions = ['propertychange', 'change', 'input', 'paste'];

      for (var i = 0, j = actions.length; i < j; i++) {
        this.input.addEventListener(actions[i], this.handleQuotes, false);
      }
    }
    // Check for deletions
    this.input.onkeydown = function(event) {
      var key = event.keyCode || event.charCode;
      if ( key == 8 || key == 46 ) {
        TypeBetter.handleDeletions();
      }
    };
  },

  handleDeletions: function() {
    var deletedSpot = TypeBetter.input.selectionStart - 1,
        deletedChar = TypeBetter.newValue[deletedSpot];
    switch (deletedChar) {
      case '\u201c':
      case '\u201d':
      case '\u2018':
      case '\u2019':
      case '\u2032':
      case '\u2033':
      case '\u2034':
        if (TypeBetter.deletions.indexOf(deletedSpot) <= -1) {
          TypeBetter.deletions.push(deletedSpot);
        }
        return true;
        break;
      default:
        return false;
    }
  },

  handleQuotes: function() {
    // get current cursor position
    var start     = this.selectionStart,
        end       = this.selectionEnd,
        tempValue = TypeBetter.input.value;
    // Create array of deleted characters
    if (TypeBetter.deletions.length > 0) {
      // Loop through position of deleted characters
      for (i = 0; i < TypeBetter.deletions.length; i++) {
        var tempPosition = TypeBetter.deletions[i];
        // Move deleted character spot, if necessary
        if (start < tempPosition) {
          // If this happens before our deletion
          // Add one to the position
          tempPosition = tempPosition++;
        }
        // Add chars to array
        TypeBetter.deletedChars[i] = {"tempPosition": tempPosition, "tempChar": tempValue[tempPosition]};
        // Remove characters from input string
        tempValue = tempValue.substring(0, tempPosition) + tempValue.substring(tempPosition + 1, tempValue.length);
      }
    }
    // Replaces input field type with better type
    TypeBetter.newValue = tempValue
      // beginning "
      .replace(/(\W|^)"/g, '$1\u201c')
      // ending "
      .replace(/(\u201c|"[^"]*)([^"]*)(\u201c|"[^"]*)/g, '$1$2\u201d')
      // beginning '
      .replace(/(\W|^)'(\S)/g, '$1\u2018$2')
      .replace(/(\W|^)\u2032(\S)/g,'$1\u2018$2')
      .replace(/(\W|^)\u2019(\S)/g,'$1\u2018$2')
      // conjunction's possession
      .replace(/([a-z])'([a-z])/ig, '$1\u2019$2')
      // ending '
      .replace(/((\u2018[^']*)|[a-z])'([^0-9]|$)/ig, '$1\u2019$3')
      // backwards apostrophe
      .replace(/(\B|^)\u2018(?=([^\u2019]*\u2019\b)*([^\u2019\u2018]*\W[\u2019\u2018]\b|[^\u2019\u2018]*$))/ig, '$1\u2019')
      // abbreviated years like '82
      .replace(/(\u2019|')([0-9]{2}[^\u2019]*)(\u2018([^0-9]|$)|$|\u2019[a-z])/ig, '\u2019$2$3')
      // triple prime
      .replace(/'''/g, '\u2034')
      // double prime
      .replace(/("|'')/g, '\u2033')
      // prime
      .replace(/'/g, '\u2032')
      // hack for prime end quote
      .replace(/(\u2032|′)("|”|“)/g, '′”')
      // hack for apostrophe end quote
      .replace(/(\u2019|\u2018)("|”|“)/g, '’”')
      // ellipses
      .replace(/(\.\.\.)/, '\u2026')
      // mdash
      .replace(/\u2013\u2013|--/g, ' \u2014 ')
      // rock 'n' roll
      .replace(/(\s)(\u2018|')(n)(\u2019|')(\s)/gi, ' ’n’ ');
    if (TypeBetter.deletions.length > 0) {
      // Loop through deleted characters
      for (var key in TypeBetter.deletedChars) {
        // Set this as a local var to make it easier to work with
        var deletions = TypeBetter.deletedChars[key];
        // Readd deleted characters
        TypeBetter.newValue = [TypeBetter.newValue.slice(0, deletions.tempPosition), deletions.tempChar, TypeBetter.newValue.slice(deletions.tempPosition)].join('');
      }
    }
    // Replace headline
    TypeBetter.input.value = TypeBetter.newValue;
    // Restore cursor position
    this.setSelectionRange(start, end);
  }

};
<|endoftext|>


<commit_before>var Filter = require('broccoli-filter');

function HtmlbarsMinifier(inputTree, options) {
    if (!(this instanceof HtmlbarsMinifier)) {
        return new HtmlbarsMinifier(inputTree, options);
    }
    this.options = options;

    Filter.call(this, inputTree);

    this.inputTree = inputTree;
}

HtmlbarsMinifier.prototype = Object.create(Filter.prototype);
HtmlbarsMinifier.prototype.constructor = HtmlbarsMinifier;
HtmlbarsMinifier.prototype.extensions = ['hbs'];
HtmlbarsMinifier.prototype.targetExtension = 'hbs';
HtmlbarsMinifier.prototype.processString = function (str) {
    if (this.options.removeSpacesAroundTags){
        str = str
            // Replaces:
            //   - {{#foo}} => {{~#foo}}
            //   - {{/foo}} => {{~/foo}}
            .replace(/{{(#|\/|else)/g, '{{~$1')

            // Replaces:
            //   - ">           foo" => ">foo"
            .replace(/>\s+/g, '>')

            // Replaces:
            //   - "foo           <" => "foo<"
            .replace(/\s+</g, '<')

            // Replaces:
            //   - "}}      <" => "}}<"
            .replace(/}}\s+</g, '}}<')

            // Replaces:
            //   - ">      {{" => ">{{"
            .replace(/>\s+{{/g, '>{{')
    }

    if (this.options.coalesceSpaces){
        // Replaces:
        //   - ">           foo" => "> foo"
        str = str.replace(/\s{2,}/g, ' ');
    }

    if (this.options.stripIndentation){
        // Replaces:
        //   - "     foo" => "foo"
        str = str.replace(/^\s+/, '');
    }

    if (this.options.removeTrailingSpaces){
        // Replaces:
        //   - "foo     " => "foo"
        str = str.replace(/\s+$/, '');
    }

    if (this.options.stripNewlines){
        str = str
            // Replaces:
            //   - "

            //      foo
" => "
foo
"
            .replace(/(>\s*)[\r\n]+/g, '$1')

            // Replaces:
            //   - "{{#block}}
            //      foo{{/block}}" => "{{#block}}foo{{/block}}"
            //   - "{{helper}}
            //      foo" => "{{helper}}foo"
            .replace(/(}}\s*)[\r\n]+/g, '$1')

            // Replaces:
            //   - "
foo
            //
" => "
foo
"
            //   - "foo
            //
" => "foo
"
            .replace(/[\r\n]+(\s*<)/g, '$1')

            // Replaces:
            //   - "{{#block}}foo
            //      {{/block}}" => "{{#block}}foo{{/block}}"
            //   - "foo
            //      {{helper}}" => "foo{{helper}}"
            .replace(/[\r\n]+(\s*{{)/g, '$1')
    }

    return str;
};

module.exports = HtmlbarsMinifier;
<commit_msg>These replaces seemingly always needed the multiline and global flags but lacked them. It wasn't so important before, since some of the replaces made each other redundant before.<commit_after>var Filter = require('broccoli-filter');

function HtmlbarsMinifier(inputTree, options) {
    if (!(this instanceof HtmlbarsMinifier)) {
        return new HtmlbarsMinifier(inputTree, options);
    }
    this.options = options;

    Filter.call(this, inputTree);

    this.inputTree = inputTree;
}

HtmlbarsMinifier.prototype = Object.create(Filter.prototype);
HtmlbarsMinifier.prototype.constructor = HtmlbarsMinifier;
HtmlbarsMinifier.prototype.extensions = ['hbs'];
HtmlbarsMinifier.prototype.targetExtension = 'hbs';
HtmlbarsMinifier.prototype.processString = function (str) {
    if (this.options.removeSpacesAroundTags){
        str = str
            // Replaces:
            //   - {{#foo}} => {{~#foo}}
            //   - {{/foo}} => {{~/foo}}
            .replace(/{{(#|\/|else)/g, '{{~$1')

            // Replaces:
            //   - ">           foo" => ">foo"
            .replace(/>\s+/g, '>')

            // Replaces:
            //   - "foo           <" => "foo<"
            .replace(/\s+</g, '<')

            // Replaces:
            //   - "}}      <" => "}}<"
            .replace(/}}\s+</g, '}}<')

            // Replaces:
            //   - ">      {{" => ">{{"
            .replace(/>\s+{{/g, '>{{')
    }

    if (this.options.coalesceSpaces){
        // Replaces:
        //   - ">           foo" => "> foo"
        str = str.replace(/\s{2,}/g, ' ');
    }

    if (this.options.stripIndentation){
        // Replaces:
        //   - "     foo" => "foo"
        str = str.replace(/^\s+/mg, '');
    }

    if (this.options.removeTrailingSpaces){
        // Replaces:
        //   - "foo     " => "foo"
        str = str.replace(/\s+$/mg, '');
    }

    if (this.options.stripNewlines){
        str = str
            // Replaces:
            //   - "

            //      foo
" => "
foo
"
            .replace(/(>\s*)[\r\n]+/g, '$1')

            // Replaces:
            //   - "{{#block}}
            //      foo{{/block}}" => "{{#block}}foo{{/block}}"
            //   - "{{helper}}
            //      foo" => "{{helper}}foo"
            .replace(/(}}\s*)[\r\n]+/g, '$1')

            // Replaces:
            //   - "
foo
            //
" => "
foo
"
            //   - "foo
            //
" => "foo
"
            .replace(/[\r\n]+(\s*<)/g, '$1')

            // Replaces:
            //   - "{{#block}}foo
            //      {{/block}}" => "{{#block}}foo{{/block}}"
            //   - "foo
            //      {{helper}}" => "foo{{helper}}"
            .replace(/[\r\n]+(\s*{{)/g, '$1')
    }

    return str;
};

module.exports = HtmlbarsMinifier;
<|endoftext|>


<commit_before>// MIT License
//
// Copyright (c) 2017 Eric Laspe
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// Ported to Processing.js from my Khan Academy project, here:
// https://www.khanacademy.org/computer-programming/brick-brick-gear-pixel-creator/5760802139734016

var canvasDimX = 600;
var canvasDimY = 600;

var cOrange = color(255, 132, 0);
var cYellow = color(238, 255, 0);
var cBrown = color(161, 106, 47);
var cBlack = color(0, 0, 0);
var cPink = color(254, 187, 223);
var cBlue = color(41, 59, 255);
var cNavy = color(0, 12, 117);
var cGray = color(140, 140, 140);
var cPeach = color(255, 206, 122);
var cGreen = color(0, 184, 0);
var cPurple = color(144, 0, 196);
var cRed = color(255, 0, 0);
var cWhite = color(255, 255, 255);
var cErase = color(50, 50, 50);
var cTransparent = color(255, 0, 0, 0);

var paletteColors = [
    cWhite, cBlack, cGray, cNavy, cBlue,
    cYellow, cPeach, cOrange, cRed, cPurple,
    cGreen, cBrown, cPink, cErase
];

var paletteOffsetX = 125;
var paletteOffsetY = 320;
var paletteNCols = 5;
var palettePSize = 70;
var paletteLen = paletteColors.length;
var paletteSelected = 0;

var boardBgColor = cBlack + 1;
var boardStColor = color(40, 40, 40);

var drawColor = paletteColors[0];
var nCols = 36;
var nRows = 16;
var nPixels = nCols * nRows;
var pSize = floor(600 / nCols);
var offset = 12;
var pA = [nPixels];
var pCounts = [paletteLen];

var f = createFont("monospace");
textFont(f);

var paletteGetLowerBoundX = function(i) {
    return palettePSize * (i % paletteNCols) + paletteOffsetX;
};

var paletteGetLowerBoundY = function(i) {
    return palettePSize * floor(i / paletteNCols) + paletteOffsetY;
};

var paletteDraw = function() {
    var textStr;
    var currentColor;

    strokeWeight(2);
    stroke(cWhite);
    fill(cWhite);

    rect(paletteGetLowerBoundX(0) - 1,
         paletteGetLowerBoundY(0) - 1,
         palettePSize * paletteNCols + 2,
         palettePSize * 3 + 2);

    for (var i = 0; i < paletteLen; i++) {
        currentColor = paletteColors[i];
        strokeWeight(2);
        stroke(cWhite);
        fill(currentColor);
        rect(paletteGetLowerBoundX(i),
             paletteGetLowerBoundY(i),
             palettePSize, palettePSize);

        if (currentColor === cErase)
            textStr = 'Eraser';
        else
            textStr = pCounts[i];

        // Print counts in contrasting color
        if (currentColor <= cErase) {
            fill(255, 255, 255);
        } else {
            fill(40, 40, 40);
        }
        textSize(14);
        text(textStr,
             paletteGetLowerBoundX(i) + palettePSize * 0.05,
             paletteGetLowerBoundY(i) + palettePSize * 0.2);
    }

    // Stroke selected color in white
    strokeWeight(4);
    stroke(cBlack);
    fill(cTransparent);

    rect(paletteGetLowerBoundX(paletteSelected),
         paletteGetLowerBoundY(paletteSelected),
         palettePSize, palettePSize);
};

var updatePixelCounts = function() {
    var c, i;
    for (c = 0; c < paletteLen; c++) {
        pCounts[c] = 0;
    }

    for (i = 0; i < nPixels; i++) {
        for (c = 0; c < paletteLen; c++) {
            if (pA[i] === paletteColors[c]) {
                ++pCounts[c];
            }
        }
    }

    paletteDraw();
};

var paletteHit = function(mX, mY) {
    var hitColor;
    var i;
    var lowerX, lowerY;

    for (var i = 0; i < paletteLen; i++) {
        lowerX = paletteGetLowerBoundX(i);
        lowerY = paletteGetLowerBoundY(i);

        if (mX >= lowerX && mX < lowerX + palettePSize &&
            mY >= lowerY && mY < lowerY + palettePSize) {

            hitColor = paletteColors[i];
            drawColor = hitColor;
            paletteSelected = i;
            paletteDraw();
            return true;
        }
    }

    return false;
};

// Based on the linear gradient here:
// http://processingjs.org/learning/basic/lineargradient/
var setGradient = function(x, y, w, h) {
    var i;
    var j;
    var fromColor = cGray;
    var toColor = cWhite;
    var c;
    var endCol = x + w;
    var endRow = y + h;

    // calculate differences between color components
    var deltaR = red(toColor) - red(fromColor);
    var deltaG = green(toColor) - green(fromColor);
    var deltaB = blue(toColor) - blue(fromColor);

    // Set pixels
    for (i = x; i <= endCol; i++) {
        for (j = y; j <= endRow; j++) {
            c = color((red(fromColor)+j*(deltaR/h)),
                      (green(fromColor)+j*(deltaG/h)),
                      (blue(fromColor)+j*(deltaB/h)));
            set(i, j, c);
        }
    }
};

var boardGetLowerBound = function(i) {
    return floor(i * pSize + offset);
};

var numberColumns = function() {
    var i;

    fill(cGray);
    textSize(10);

    // Top edge of board
    text('1', pSize, 11);
    for (i = 0; i <= nCols; i += 4) {
        if (i === 0)
            continue;
        text(i, i * pSize, 11);
    }

    // Bottom edge of board
    text('1', pSize, (nRows + 1) * pSize);
    for (i = 0; i <= nCols; i += 4) {
        if (i === 0)
            continue;
        text(i, i * pSize, (nRows + 1) * pSize + 4);
    }
};

var numberRows = function(i) {
    var i;

    fill(cGray);
    textSize(10);

    // Left side of board
    text('1', 7, pSize + 8);
    for (i = 0; i <= nRows; i += 4) {
        if (i === 0)
            continue;
        if (i < 10)
            text(i, 7, i * pSize + 8);
        else
            text(i, 1, i * pSize + 8);
    }

    // Right side of board
    text('1', canvasDimX - 13, pSize + 8);
    for (i = 0; i <= nRows; i += 4) {
        if (i === 0)
            continue;
        text(i, canvasDimX - 13, i * pSize + 8);
    }
};

var boardInit = function() {
    for (var i = 0; i < nCols; i++) {
        for (var j = 0; j < nRows; j++) {

            // Init each pixel color
            pA[nCols * j + i] = boardBgColor;
        }
    }

    updatePixelCounts();
    boardDraw();
};

var boardBgDraw = function() {
    fill(boardBgColor);
    rect(0, 0, 600, pSize * (nRows + 2) - 8, 16);
};

var boardDraw = function() {
    var i, j;
    var pixelColor = pA
    var cornerX, cornerY;
    var circleOffset = pSize/2;
    var circleDiam = 8;
    var LabelNum = 1;

    strokeWeight(1);
    noStroke();

    boardBgDraw();
    numberColumns();
    numberRows();

    for (i = 0; i < nCols; i++) {
        for (j = 0; j < nRows; j++) {

            // Get pixel parameters
            pixelColor = pA[nCols * j + i];
            cornerX = boardGetLowerBound(i);
            cornerY = boardGetLowerBound(j);

            if (pixelColor === boardBgColor)
                noStroke();
            else
                stroke(boardStColor);

            // Draw pixel
            fill(pixelColor);
            rect(cornerX,
                 cornerY,
                 pSize, pSize);

            // Draw the nib
            if (pixelColor === boardBgColor)
                stroke(boardStColor);
            else if (pixelColor === cBlack)
                stroke(color(70, 70, 70));
            else
                stroke(cBlack);

            ellipse(cornerX + circleOffset + 1,
                    cornerY + circleOffset + 1,
                    circleDiam, circleDiam);
            ellipse(cornerX + circleOffset,
                    cornerY + circleOffset,
                    circleDiam, circleDiam);
        }
    }

};

var boardHit = function(mX, mY) {
    var i, j;
    var lowerX, lowerY;
    var pixelColor;

    for (i = 0; i < nCols; i++) {
        for (j = 0; j < nRows; j++) {
            lowerX = boardGetLowerBound(i);
            lowerY = boardGetLowerBound(j);

            if (mX >= lowerX && mX < lowerX + pSize &&
                mY >= lowerY && mY < lowerY + pSize) {

                pixelColor = drawColor;
                if (drawColor === cErase) {
                    pixelColor = boardBgColor;
                }
                pA[nCols * j + i] = pixelColor;
                updatePixelCounts();
                boardDraw();
                return;
            }
        }
    }
};

void mouseDragged() {
    // Only check for board hit on drag
    var pixHit = boardHit(mouseX, mouseY);
};

void mousePressed() {
    // Only check for board hit on drag
    var pixHit = boardHit(mouseX, mouseY);
};

void mouseReleased() {
    // First check for palette hit
    if (paletteHit(mouseX, mouseY)) {
        return;
    }

    // Next check for board hit
    boardHit(mouseX, mouseY);
};

void setup() {
    size(canvasDimX, canvasDimY);
    background(cWhite);
    setGradient(0, canvasDimY * 0.4, canvasDimX, canvasDimY * 0.9);

    boardInit();
    paletteDraw();
}
<commit_msg>Draw empty nibs more efficiently<commit_after>// MIT License
//
// Copyright (c) 2017 Eric Laspe
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// Ported to Processing.js from my Khan Academy project, here:
// https://www.khanacademy.org/computer-programming/brick-brick-gear-pixel-creator/5760802139734016

var canvasDimX = 600;
var canvasDimY = 600;

var cOrange = color(255, 132, 0);
var cYellow = color(238, 255, 0);
var cBrown = color(161, 106, 47);
var cBlack = color(0, 0, 0);
var cPink = color(254, 187, 223);
var cBlue = color(41, 59, 255);
var cNavy = color(0, 12, 117);
var cGray = color(140, 140, 140);
var cPeach = color(255, 206, 122);
var cGreen = color(0, 184, 0);
var cPurple = color(144, 0, 196);
var cRed = color(255, 0, 0);
var cWhite = color(255, 255, 255);
var cErase = color(50, 50, 50);
var cTransparent = color(255, 0, 0, 0);

var paletteColors = [
    cWhite, cBlack, cGray, cNavy, cBlue,
    cYellow, cPeach, cOrange, cRed, cPurple,
    cGreen, cBrown, cPink, cErase
];

var paletteOffsetX = 125;
var paletteOffsetY = 320;
var paletteNCols = 5;
var palettePSize = 70;
var paletteLen = paletteColors.length;
var paletteSelected = 0;

var boardBgColor = cBlack + 1;
var boardStColor = color(40, 40, 40);

var drawColor = paletteColors[0];
var nCols = 36;
var nRows = 16;
var nPixels = nCols * nRows;
var pSize = floor(600 / nCols);
var offset = 12;
var pA = [nPixels];
var pCounts = [paletteLen];

var f = createFont("monospace");
textFont(f);

var paletteGetLowerBoundX = function(i) {
    return palettePSize * (i % paletteNCols) + paletteOffsetX;
};

var paletteGetLowerBoundY = function(i) {
    return palettePSize * floor(i / paletteNCols) + paletteOffsetY;
};

var paletteDraw = function() {
    var textStr;
    var currentColor;

    strokeWeight(2);
    stroke(cWhite);
    fill(cWhite);

    rect(paletteGetLowerBoundX(0) - 1,
         paletteGetLowerBoundY(0) - 1,
         palettePSize * paletteNCols + 2,
         palettePSize * 3 + 2);

    for (var i = 0; i < paletteLen; i++) {
        currentColor = paletteColors[i];
        strokeWeight(2);
        stroke(cWhite);
        fill(currentColor);
        rect(paletteGetLowerBoundX(i),
             paletteGetLowerBoundY(i),
             palettePSize, palettePSize);

        if (currentColor === cErase)
            textStr = 'Eraser';
        else
            textStr = pCounts[i];

        // Print counts in contrasting color
        if (currentColor <= cErase) {
            fill(255, 255, 255);
        } else {
            fill(40, 40, 40);
        }
        textSize(14);
        text(textStr,
             paletteGetLowerBoundX(i) + palettePSize * 0.05,
             paletteGetLowerBoundY(i) + palettePSize * 0.2);
    }

    // Stroke selected color in white
    strokeWeight(4);
    stroke(cBlack);
    fill(cTransparent);

    rect(paletteGetLowerBoundX(paletteSelected),
         paletteGetLowerBoundY(paletteSelected),
         palettePSize, palettePSize);
};

var updatePixelCounts = function() {
    var c, i;
    for (c = 0; c < paletteLen; c++) {
        pCounts[c] = 0;
    }

    for (i = 0; i < nPixels; i++) {
        for (c = 0; c < paletteLen; c++) {
            if (pA[i] === paletteColors[c]) {
                ++pCounts[c];
            }
        }
    }

    paletteDraw();
};

var paletteHit = function(mX, mY) {
    var hitColor;
    var i;
    var lowerX, lowerY;

    for (var i = 0; i < paletteLen; i++) {
        lowerX = paletteGetLowerBoundX(i);
        lowerY = paletteGetLowerBoundY(i);

        if (mX >= lowerX && mX < lowerX + palettePSize &&
            mY >= lowerY && mY < lowerY + palettePSize) {

            hitColor = paletteColors[i];
            drawColor = hitColor;
            paletteSelected = i;
            paletteDraw();
            return true;
        }
    }

    return false;
};

// Based on the linear gradient here:
// http://processingjs.org/learning/basic/lineargradient/
var setGradient = function(x, y, w, h) {
    var i;
    var j;
    var fromColor = cGray;
    var toColor = cWhite;
    var c;
    var endCol = x + w;
    var endRow = y + h;

    // calculate differences between color components
    var deltaR = red(toColor) - red(fromColor);
    var deltaG = green(toColor) - green(fromColor);
    var deltaB = blue(toColor) - blue(fromColor);

    // Set pixels
    for (i = x; i <= endCol; i++) {
        for (j = y; j <= endRow; j++) {
            c = color((red(fromColor)+j*(deltaR/h)),
                      (green(fromColor)+j*(deltaG/h)),
                      (blue(fromColor)+j*(deltaB/h)));
            set(i, j, c);
        }
    }
};

var boardGetLowerBound = function(i) {
    return floor(i * pSize + offset);
};

var numberColumns = function() {
    var i;

    fill(cGray);
    textSize(10);

    // Top edge of board
    text('1', pSize, 11);
    for (i = 0; i <= nCols; i += 4) {
        if (i === 0)
            continue;
        text(i, i * pSize, 11);
    }

    // Bottom edge of board
    text('1', pSize, (nRows + 1) * pSize);
    for (i = 0; i <= nCols; i += 4) {
        if (i === 0)
            continue;
        text(i, i * pSize, (nRows + 1) * pSize + 4);
    }
};

var numberRows = function(i) {
    var i;

    fill(cGray);
    textSize(10);

    // Left side of board
    text('1', 7, pSize + 8);
    for (i = 0; i <= nRows; i += 4) {
        if (i === 0)
            continue;
        if (i < 10)
            text(i, 7, i * pSize + 8);
        else
            text(i, 1, i * pSize + 8);
    }

    // Right side of board
    text('1', canvasDimX - 13, pSize + 8);
    for (i = 0; i <= nRows; i += 4) {
        if (i === 0)
            continue;
        text(i, canvasDimX - 13, i * pSize + 8);
    }
};

var boardInit = function() {
    for (var i = 0; i < nCols; i++) {
        for (var j = 0; j < nRows; j++) {

            // Init each pixel color
            pA[nCols * j + i] = boardBgColor;
        }
    }

    updatePixelCounts();
    boardDraw();
};

var boardBgDraw = function() {
    fill(boardBgColor);
    rect(0, 0, 600, pSize * (nRows + 2) - 8, 16);
};

var boardDraw = function() {
    var i, j;
    var pixelColor = pA
    var cornerX, cornerY;
    var circleOffset = pSize/2;
    var circleDiam = 8;
    var LabelNum = 1;

    strokeWeight(1);
    noStroke();

    boardBgDraw();
    numberColumns();
    numberRows();

    for (i = 0; i < nCols; i++) {
        for (j = 0; j < nRows; j++) {

            // Get pixel parameters
            pixelColor = pA[nCols * j + i];
            cornerX = boardGetLowerBound(i);
            cornerY = boardGetLowerBound(j);

            fill(boardBgColor);
            stroke(boardStColor);

            // Draw brick
            if (pixelColor !== boardBgColor) {
                if (pixelColor === cBlack) {
                    stroke(color(70, 70, 70));
                } else {
                    stroke(cBlack);
                }
                fill(pixelColor);
                rect(cornerX, cornerY, pSize, pSize);
            }

            // Draw nib
            ellipse(cornerX + circleOffset + 1,
                    cornerY + circleOffset + 1,
                    circleDiam, circleDiam);
            ellipse(cornerX + circleOffset,
                    cornerY + circleOffset,
                    circleDiam, circleDiam);
        }
    }

};

var boardHit = function(mX, mY) {
    var i, j;
    var lowerX, lowerY;
    var pixelColor;

    for (i = 0; i < nCols; i++) {
        for (j = 0; j < nRows; j++) {
            lowerX = boardGetLowerBound(i);
            lowerY = boardGetLowerBound(j);

            if (mX >= lowerX && mX < lowerX + pSize &&
                mY >= lowerY && mY < lowerY + pSize) {

                pixelColor = drawColor;
                if (drawColor === cErase) {
                    pixelColor = boardBgColor;
                }
                pA[nCols * j + i] = pixelColor;
                updatePixelCounts();
                boardDraw();
                return;
            }
        }
    }
};

void mouseDragged() {
    // Only check for board hit on drag
    var pixHit = boardHit(mouseX, mouseY);
};

void mousePressed() {
    // Only check for board hit on drag
    var pixHit = boardHit(mouseX, mouseY);
};

void mouseReleased() {
    // First check for palette hit
    if (paletteHit(mouseX, mouseY)) {
        return;
    }

    // Next check for board hit
    boardHit(mouseX, mouseY);
};

void setup() {
    size(canvasDimX, canvasDimY);
    background(cWhite);
    setGradient(0, canvasDimY * 0.4, canvasDimX, canvasDimY * 0.9);

    boardInit();
    paletteDraw();
}
<|endoftext|>


<commit_before>var socket = io()

socket.on('update-t', function() {
  listT()
})

socket.on('finish-t', function(hash) {
  $('.torrent[hash=' + hash + ']').remove()
})

socket.on('list-t', function(torrents) {
  listTorrent(torrents)
})

socket.on('list-d', function(directory) {
  appendDirectorySize(directory.totalSize)
  $.each($('.container .directory .list tbody *'), function(key, value) {
    $(value).addClass('toremove')
  })
  listDirectory(directory.files)
  $('.toremove').remove()
})

socket.on('update-d', function() {
  listD()
})

socket.on('start-t', function(data) {
  var notif = new Pnotif()
  notif.init('top-right', "
Le torrent va commencer dans quelques instants.
", 10000)
  notif.draw()
})

socket.on('search-t', function(data) {
  var $searchDiv = $('.menu .search-result')

  if ($('.search-result .close-search').length <= 0) {
    var $closeBut = $('<button>').addClass('close-search').text('x').click(function() {
      $searchDiv.html('')
    }).appendTo($searchDiv)
  }

  if ($('.search-result .films').length <= 0) {
    $('
').addClass('films').html('
Films
').appendTo($searchDiv)
  }
  var $films = $('.search-result .films')

  if ($('.search-result .series').length <= 0) {
    $('
').addClass('series').html('
Series
').appendTo($searchDiv)
  }
  var $series = $('.search-result .series')

  data.data.items.forEach(function(element, index) {
    var $item = $('
').addClass('search-item').attr('torrent-link', element.torrent)
    $('').text(element.title).appendTo($item)
    $('').text(element.size).appendTo($item)
    $('').html($('').addClass('fa fa-arrow-up').text(element.seeds)).appendTo($item)
    $('').html($('').addClass('fa fa-arrow-down').text(element.leechs)).appendTo($item)

    if (data.type == 'films') {
      $item.appendTo($films)
    } else if (data.type == 'series') {
      $item.appendTo($series)
    }

  })
})

socket.on('error-t', function(hash) {
  $('.torrent[hash=' + hash + ']').remove()
  var notif = new Pnotif()
  notif.init('top-right', "
Erreur avec le torrent. Nouvelle tentative dans quelques instants
", 10000)
  notif.draw()
})

socket.on('infos-d', function(data){
  console.log(data)
})
<commit_msg>prepare popup usage<commit_after>var socket = io()

socket.on('update-t', function() {
  listT()
})

socket.on('finish-t', function(hash) {
  $('.torrent[hash=' + hash + ']').remove()
})

socket.on('list-t', function(torrents) {
  listTorrent(torrents)
})

socket.on('list-d', function(directory) {
  appendDirectorySize(directory.totalSize)
  $.each($('.container .directory .list tbody *'), function(key, value) {
    $(value).addClass('toremove')
  })
  listDirectory(directory.files)
  $('.toremove').remove()
})

socket.on('update-d', function() {
  listD()
})

socket.on('start-t', function(data) {
  var notif = new Pnotif()
  notif.init('top-right', "
Le torrent va commencer dans quelques instants.
", 10000)
  notif.draw()
})

socket.on('search-t', function(data) {
  var $searchDiv = $('.menu .search-result')

  if ($('.search-result .close-search').length <= 0) {
    var $closeBut = $('<button>').addClass('close-search').text('x').click(function() {
      $searchDiv.html('')
    }).appendTo($searchDiv)
  }

  if ($('.search-result .films').length <= 0) {
    $('
').addClass('films').html('
Films
').appendTo($searchDiv)
  }
  var $films = $('.search-result .films')

  if ($('.search-result .series').length <= 0) {
    $('
').addClass('series').html('
Series
').appendTo($searchDiv)
  }
  var $series = $('.search-result .series')

  data.data.items.forEach(function(element, index) {
    var $item = $('
').addClass('search-item').attr('torrent-link', element.torrent)
    $('').text(element.title).appendTo($item)
    $('').text(element.size).appendTo($item)
    $('').html($('').addClass('fa fa-arrow-up').text(element.seeds)).appendTo($item)
    $('').html($('').addClass('fa fa-arrow-down').text(element.leechs)).appendTo($item)

    if (data.type == 'films') {
      $item.appendTo($films)
    } else if (data.type == 'series') {
      $item.appendTo($series)
    }

  })
})

socket.on('error-t', function(hash) {
  $('.torrent[hash=' + hash + ']').remove()
  var notif = new Pnotif()
  notif.init('top-right', "
Erreur avec le torrent. Nouvelle tentative dans quelques instants
", 10000)
  notif.draw()
})

socket.on('infos-d', function(data){
  var p = new Popup()
  p.init(null,null,null,null,"Infos","

",true)
  p.draw()
  console.log(data)
})
<|endoftext|>


<commit_before>require('../support')
import { mocks, helpers } from '../../support'
import Login from '../../../src/containers/Login.js'
import { FETCH_COMMUNITY } from '../../../src/actions'
import { renderIntoDocument } from 'react-addons-test-utils'
const { createElement } = helpers
import { HOST } from '../../../src/util/api'
import nock from 'nock'
import { configureStore } from '../../../src/store'
import { getUrlFromLocation } from '../../../src/util/navigation'

const redirectUrl = store => {
  const location = store.getState().routing.locationBeforeTransitions
  return getUrlFromLocation(location)
}

describe('Login', () => {
  describe('on successful login', () => {
    var store, node

    const setup = (loginResponse, props) => {
      nock(HOST).post('/login').reply(200, loginResponse)
      const component = createElement(Login, props, {store})
      node = renderIntoDocument(component).getWrappedInstance()
    }

    beforeEach(() => {
      store = configureStore({}).store
    })

    it("redirects to the person's profile by default", () => {
      setup({id: 42}, {location: {query: {}}})
      return node.submit(mocks.event())
      .then(() => {
        expect(redirectUrl(store)).to.equal('/u/42')
        expect(window.analytics.alias).to.have.been.called()
      })
    })

    it('redirects to the last visited community if available', () => {
      const slug = 'foomunity'
      const communityId = '5'
      nock(HOST).get('/noo/community/foomunity').reply(200, {slug})
      const user = {
        id: 42,
        settings: {currentCommunityId: communityId},
        memberships: [{community_id: communityId, community: {id: communityId, slug}}]
      }
      setup(user, {location: {query: {}}})
      return node.submit(mocks.event())
      .then(() => expect(redirectUrl(store)).to.equal(`/c/${slug}`))
    })

    it('redirects to a specified location', () => {
      setup({id: 42}, {location: {query: {next: '/c/sandbox'}}})
      return node.submit(mocks.event())
      .then(() => expect(redirectUrl(store)).to.equal('/c/sandbox'))
    })
  })
})
<commit_msg>fix tests<commit_after>require('../support')
import { mocks, helpers } from '../../support'
import Login from '../../../src/containers/Login.js'
import { renderIntoDocument } from 'react-addons-test-utils'
const { createElement } = helpers
import { HOST } from '../../../src/util/api'
import nock from 'nock'
import { configureStore } from '../../../src/store'
import { getUrlFromLocation } from '../../../src/util/navigation'

const redirectUrl = store => {
  const location = store.getState().routing.locationBeforeTransitions
  return getUrlFromLocation(location)
}

describe('Login', () => {
  describe('on successful login', () => {
    var store, node

    const setup = (loginResponse, props) => {
      nock(HOST).post('/login').reply(200, loginResponse)
      const component = createElement(Login, props, {store})
      node = renderIntoDocument(component).getWrappedInstance()
    }

    beforeEach(() => {
      store = configureStore({}).store
    })

    it("redirects to the person's profile by default", () => {
      setup({id: 42, memberships: {id: 1}}, {location: {query: {}}})
      return node.submit(mocks.event())
      .then(() => {
        expect(redirectUrl(store)).to.equal('/u/42')
        expect(window.analytics.alias).to.have.been.called()
      })
    })

    it('redirects to the community creation form if the user has no communities', () => {
      setup({id: 42}, {location: {query: {}}})
      return node.submit(mocks.event())
      .then(() => {
        expect(redirectUrl(store)).to.equal('/create')
        expect(window.analytics.alias).to.have.been.called()
      })
    })

    it('redirects to the last visited community if available', () => {
      const slug = 'foomunity'
      const communityId = '5'
      nock(HOST).get('/noo/community/foomunity').reply(200, {slug})
      const user = {
        id: 42,
        settings: {currentCommunityId: communityId},
        memberships: [{community_id: communityId, community: {id: communityId, slug}}]
      }
      setup(user, {location: {query: {}}})
      return node.submit(mocks.event())
      .then(() => expect(redirectUrl(store)).to.equal(`/c/${slug}`))
    })

    it('redirects to a specified location', () => {
      setup({id: 42}, {location: {query: {next: '/c/sandbox'}}})
      return node.submit(mocks.event())
      .then(() => expect(redirectUrl(store)).to.equal('/c/sandbox'))
    })
  })
})
<|endoftext|>