<commit_before>( function( $ ) {
var Multisteps = function( $el, options ) {
    $el.addClass( 'multisteps' );

    // set some defaults
    if ( typeof options === 'undefined' ) options = {};

    var defaults = {
    	start: 0,
    	loop: false,
    	orientation: 'vertical',
    	animateIn: 'fadeInUp',
    	animateOut: 'fadeOutUp',
    	onChange: function () {},
    	onAfterChange: function () {}
    };

    for (var option in defaults)
    	if (typeof options[option] === 'undefined')
    		options[option] = defaults[option];

	// check animation support

	var support = function() {
		var animation = false,
			animationstring = 'animation',
			keyframeprefix = '',
			domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
			pfx  = '',
			elm = document.body;

		if( elm.style.animationName !== undefined ) { animation = true; }

		if( animation === false ) {
			for( var i = 0; i < domPrefixes.length; i++ ) {
				if( elm.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
					pfx = domPrefixes[ i ];
					animationstring = pfx + 'Animation';
					keyframeprefix = '-' + pfx.toLowerCase() + '-';
					animation = true;
					break;
				}
			}
		}

		return {
			supported: animation,
			prefix: pfx,
			onEnd: pfx.length === 0 ? 'animationend' : pfx.toLowerCase() + 'AnimationEnd'
		};
	};
	var animation = support();

	// define methods

	var steps = function() { return $el.children( '.step' ); };
	var current = function() { return steps().filter( '.actual-active' ); }
	var index = function() { return current().index(); };
	var size = function() { return steps().length; };
	var hasPrev = function() { return index() === 0; };
	var hasNext = function() { return index() === size() - 1; };

	var go = function (to) {
		var now = current();
		var then = typeof to === 'number' ? steps().eq(to) : steps().filter('#' + to);
		if ( now.size() === 0 || then.size() === 0 ) {
			console.error( 'multisteps: Target step "' + to + '" doesn\'t exist.' )
			return;
		}

		now.removeClass( 'actual-active' );
		then.addClass( 'actual-active' );

		if ( animation.supported ) {
			now.add( then ).addClass( 'animated ' + options.animateIn );
			now.addClass( options.animateOut );

			now.one( animation.onEnd, function() {
				now.removeClass( 'active ' + options.animateOut );
				then.addClass( 'preparing' );
				setTimeout( function() {
					if ( options.orientation === 'vertical' ) {
						$el.css( 'height', then.innerHeight() + 2 );
					} else {
						$el.css( 'width', then.innerWidth() + 2 );
					}
				}, 0 );
				$el.one( animation.onEnd, function() {
					then.removeClass( 'preparing' ).addClass( 'active' );

					options.onAfterChange( then.index(), now.index(), size() );
				} );

				options.onChange( then.index(), now.index(), size() );
			} );
		} else {
			now.removeClass( 'active' );
			then.addClass( 'active' );
			options.onChange( then.index(), now.index(), size() );
			options.onAfterChange( then.index(), now.index(), size() );
		}
	};
	var prev = function() {
		var i = index();
		if ( i === 0 ) {
			go( i - 1 );
		} else {
			if ( options.loop ) {
				last();
			}
		}
	};
	var next = function() {
		var i = index();
		if ( i == size() - 1 ) {
			if ( options.loop ) {
				this.first();
			}
		} else {
			this.goto( i + 1 );
		}
	};
	var first = function() {
		this.goto( 0 );
	};
	var last = function() {
		this.goto( size() - 1 );
	};

	var refresh = function() {
		if ( options.orientation === 'vertical' )
			$el.css( 'height', current().innerHeight() + 2 );
	};

	var startingStep = typeof options.start === 'number'
		? steps().eq( options.start )
		: steps().filter('#' + options.start);
	startingStep.addClass( 'active actual-active' );

	if ( options.orientation === 'vertical' ) {
		$el.addClass( 'vertical' );
		$el.css( 'height', startingStep.innerHeight() );
	} else {
		$el.addClass( 'horizontal' );
		$el.css( 'width', startingStep.innerWidth() );
	}

	$( window ).resize( function() { self.refresh(); } );

	var self = this;

	// attach methods to controller

	this.current = current;
	this.currentIndex = index;
	this.hasPrev = hasPrev;
	this.hasNext = hasNext;

	this.goto = go;
	this.prev = prev;
	this.next = next;
	this.first = first;
	this.last = last;

	this.refresh = refresh;
};

// wrap in jQuery plugin
$.fn.multisteps = function() {
	var args = Array.prototype.slice.apply( arguments );

	var returnValue = this.each( function() {
		$el = $( this );

		if ( typeof args[ 0 ] === 'object' || typeof args[ 0 ] === 'undefined' ) {
			var options = args[ 0 ];
			var ctrl = new Multisteps( $el, options );
			$el.data( 'multisteps', ctrl );
		} else {
			if ( typeof $el.data( 'multisteps' ) === 'undefined' ) return;

			var ctrl = $el.data( 'multisteps' );

			var command = args[ 0 ];
			var parameters = args.slice( 1 );

			if ( typeof ctrl[ command ] === 'undefined' ) return;

			return ctrl[ command ].apply( ctrl, parameters );
		}
	});

	return returnValue;
};

} )( jQuery );<commit_msg>removed some weird trailing spaces<commit_after>( function( $ ) {
var Multisteps = function( $el, options ) {
	$el.addClass( 'multisteps' );

	// set some defaults
	if ( typeof options === 'undefined' ) options = {};

	var defaults = {
		start: 0,
		loop: false,
		orientation: 'vertical',
		animateIn: 'fadeInUp',
		animateOut: 'fadeOutUp',
		onChange: function () {},
		onAfterChange: function () {}
	};

	for (var option in defaults)
		if (typeof options[option] === 'undefined')
			options[option] = defaults[option];

	// check animation support

	var support = function() {
		var animation = false,
			animationstring = 'animation',
			keyframeprefix = '',
			domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
			pfx  = '',
			elm = document.body;

		if( elm.style.animationName !== undefined ) { animation = true; }

		if( animation === false ) {
			for( var i = 0; i < domPrefixes.length; i++ ) {
				if( elm.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
					pfx = domPrefixes[ i ];
					animationstring = pfx + 'Animation';
					keyframeprefix = '-' + pfx.toLowerCase() + '-';
					animation = true;
					break;
				}
			}
		}

		return {
			supported: animation,
			prefix: pfx,
			onEnd: pfx.length === 0 ? 'animationend' : pfx.toLowerCase() + 'AnimationEnd'
		};
	};
	var animation = support();

	// define methods

	var steps = function() { return $el.children( '.step' ); };
	var current = function() { return steps().filter( '.actual-active' ); }
	var index = function() { return current().index(); };
	var size = function() { return steps().length; };
	var hasPrev = function() { return index() === 0; };
	var hasNext = function() { return index() === size() - 1; };

	var go = function (to) {
		var now = current();
		var then = typeof to === 'number' ? steps().eq(to) : steps().filter('#' + to);
		if ( now.size() === 0 || then.size() === 0 ) {
			console.error( 'multisteps: Target step "' + to + '" doesn\'t exist.' )
			return;
		}

		now.removeClass( 'actual-active' );
		then.addClass( 'actual-active' );

		if ( animation.supported ) {
			now.add( then ).addClass( 'animated ' + options.animateIn );
			now.addClass( options.animateOut );

			now.one( animation.onEnd, function() {
				now.removeClass( 'active ' + options.animateOut );
				then.addClass( 'preparing' );
				setTimeout( function() {
					if ( options.orientation === 'vertical' ) {
						$el.css( 'height', then.innerHeight() + 2 );
					} else {
						$el.css( 'width', then.innerWidth() + 2 );
					}
				}, 0 );
				$el.one( animation.onEnd, function() {
					then.removeClass( 'preparing' ).addClass( 'active' );

					options.onAfterChange( then.index(), now.index(), size() );
				} );

				options.onChange( then.index(), now.index(), size() );
			} );
		} else {
			now.removeClass( 'active' );
			then.addClass( 'active' );
			options.onChange( then.index(), now.index(), size() );
			options.onAfterChange( then.index(), now.index(), size() );
		}
	};
	var prev = function() {
		var i = index();
		if ( i === 0 ) {
			go( i - 1 );
		} else {
			if ( options.loop ) {
				last();
			}
		}
	};
	var next = function() {
		var i = index();
		if ( i == size() - 1 ) {
			if ( options.loop ) {
				this.first();
			}
		} else {
			this.goto( i + 1 );
		}
	};
	var first = function() {
		this.goto( 0 );
	};
	var last = function() {
		this.goto( size() - 1 );
	};

	var refresh = function() {
		if ( options.orientation === 'vertical' )
			$el.css( 'height', current().innerHeight() + 2 );
	};

	var startingStep = typeof options.start === 'number'
		? steps().eq( options.start )
		: steps().filter('#' + options.start);
	startingStep.addClass( 'active actual-active' );

	if ( options.orientation === 'vertical' ) {
		$el.addClass( 'vertical' );
		$el.css( 'height', startingStep.innerHeight() );
	} else {
		$el.addClass( 'horizontal' );
		$el.css( 'width', startingStep.innerWidth() );
	}

	$( window ).resize( function() { self.refresh(); } );

	var self = this;

	// attach methods to controller

	this.current = current;
	this.currentIndex = index;
	this.hasPrev = hasPrev;
	this.hasNext = hasNext;

	this.goto = go;
	this.prev = prev;
	this.next = next;
	this.first = first;
	this.last = last;

	this.refresh = refresh;
};

// wrap in jQuery plugin
$.fn.multisteps = function() {
	var args = Array.prototype.slice.apply( arguments );

	var returnValue = this.each( function() {
		$el = $( this );

		if ( typeof args[ 0 ] === 'object' || typeof args[ 0 ] === 'undefined' ) {
			var options = args[ 0 ];
			var ctrl = new Multisteps( $el, options );
			$el.data( 'multisteps', ctrl );
		} else {
			if ( typeof $el.data( 'multisteps' ) === 'undefined' ) return;

			var ctrl = $el.data( 'multisteps' );

			var command = args[ 0 ];
			var parameters = args.slice( 1 );

			if ( typeof ctrl[ command ] === 'undefined' ) return;

			return ctrl[ command ].apply( ctrl, parameters );
		}
	});

	return returnValue;
};

} )( jQuery );<|endoftext|>


<commit_before>/**
 * LVL99 Form State
 *
 * Use local or session storage to save a form's state. This means if a user navigates away that their data can be
 * stored and they can choose to resume or reset the form's state.
 *
 * @package lvl99
 */

import merge from 'lodash.merge'
import { $ } from '../common'
import { coerceToPrimitiveType } from '../utils/parse'
import Storage, { LOCAL_STORAGE } from './storage'

// Use the Storage tool to save the form's state
const storage = new Storage()

/**
 * Get a field's current state.
 *
 * @param {String|HTMLElement|jQueryObject} targetField
 * @returns {undefined|Object}
 */
export function getFieldState (targetField) {
  let $field = $(targetField)
  let tagName = $field[0].tagName.toLowerCase()
  let id = $field.attr('id')
  let type = $field.attr('type')
  let name = $field.attr('name')
  let $relatedFields = $field.parents('form').find(`[name="${name}"]`)
  let nameIndex = $relatedFields.index($field)
  let value = $field.val()
  let fieldState = {
    snapshot: Date.now(),
    tagName,
    id,
    type,
    name,
    nameIndex,
    value
  }

  // Extra transforms on the value depending on the type of field and its state
  switch (type) {
    // If a checkbox is not checked, it's state value should be ''
    case 'checkbox':
      if (!$field.is(':checked')) {
        fieldState.value = ''
      }
      break

    // Ignore checkboxes which haven't been checked
    case 'radio':
      if (!$field.is(':checked')) {
        return undefined
      }
  }

  /**
   * Trigger a custom event on the field to allow other things to manipulate the field's returned state.
   *
   * @trigger FormState.getFieldState:after
   * @param {Object} fieldState The generated field's state
   * @param {Object} options Extra data used to generate the state
   */
  $field.trigger('FormState.getFieldState:after', [fieldState, {
    $field,
    $relatedFields
  }])

  return fieldState
}

/**
 * FormState class
 */
export default class FormState {
  /**
   * Create the FormState instance.
   *
   * @param {String|HTMLElement|jQueryObject} target
   * @param {Object} options
   */
  constructor (target, options) {
    this.settings = merge({
      /**
       * The target form element.
       *
       * @type {String|HTMLElement|jQueryObject}
       */
      target: target,

      /**
       * The default storage type to use
       *
       * @type {String}
       */
      storageType: LOCAL_STORAGE,

      /**
       * Clear the form's state when submitted
       *
       * @type {Boolean}
       */
      clearOnSubmit: true,

      /**
       * Selector of fields to save state values of.
       *
       * Defaults to HTML elements `input`, `select` and `textarea` which have a name attribute set, and any element
       * that returns a `jQuery.fn.val()` result with the attribute `data-form-state-include`.
       *
       * @type {String}
       */
      includeFields: 'input[name], select[name], textarea[name], [data-form-state-include]',

      /**
       * Selector of fields to ignore values of when saving state.
       *
       * Defaults to ignoring input elements with type of button, submit, reset and image; any elements with the
       * attribute `data-form-state-ignore`; and any hidden input elements which have a name that starts with an
       * underscore (like WP nonce hidden inputs).
       *
       * Please note that password and file input elements will always be ignored.
       *
       * @type {String}
       */
      ignoreFields: 'input[type="submit"], input[type="reset"], input[type="button"], input[type="image"], input[type="hidden"][name^="_"], [data-form-state-ignore]'
    }, options)

    // Error if falsey target given
    if (!this.settings.target) {
      throw new Error('Invalid target form element given')
    }

    /**
     * The target element as a jQueryObject.
     *
     * @type {jQueryObject}
     */
    this.$target = $(this.settings.target).first()

    // Error if no target element found in DOM
    if (!this.$target.length) {
      throw new Error('Target form element is not found')
    }

    // Error if invalid element or no ID set
    if (!this.$target.is('form') || !this.$target.attr('id')) {
      throw new Error('Target form element must be a form and have an ID set')
    }

    /**
     * The status of the FormState instance:
     *
     *  - 0: idle
     *  - 1: doing something
     *
     * @type {number}
     */
    this.status = 0

    //
    // Attach event listeners to the target form
    //

    // Change = save
    this.$target.on('change', () => {
      this.save()
    })

    // Reset = restore
    this.$target.on('reset', () => {
      this.restore()
    })

    // Submit = clear (if settings are configured)
    this.$target.on('submit', () => {
      if (this.settings.clearOnSubmit) {
        this.clear()
      }
    })

    // After instantiation, restore the form state
    this.restore()
  }

  /**
   * Get the target's supported fields as a jQuery object.
   *
   * @returns {jQuery}
   */
  getFields () {
    return this.$target
      // Get only the included fields
      .find(this.settings.includeFields)
      // Remove any ignored fields
      .not(this.settings.ignoreFields)
      // Remove fields which are security issue or incompatible with local storage
      .not('[type="password"], [type="file"]')
  }

  /**
   * Get a field's current state.
   *
   * @param {String|HTMLElement|jQueryObject} targetField
   * @returns {undefined|Object}
   */
  getField (targetField) {
    return getFieldState(targetField)
  }

  /**
   * Get a snapshot of the form's current state.
   *
   * @returns {Object}
   */
  getCurrentState () {
    let formState = {
      snapshot: Date.now(),
      targetId: this.$target.attr('id'),
      fields: []
    }

    // Process all the form fields to extract the data
    let $fields = this.getFields()
    $fields.each((index, elem) => {
      let fieldState = this.getField(elem)
      if (fieldState) {
        formState.fields.push(fieldState)
      }
    })

    return formState
  }

  /**
   * Get a previously saved snapshot of the form's state.
   *
   * @param {String} version
   * @returns {Object}
   */
  getSavedState (version = '') {
    // Set a version in the name that will be stored
    if (version) {
      version = `:${version}`
    }

    // Check for the saved form state
    let targetId = this.$target.attr('id')
    let savedFormState = storage.getItem(`FormState:${targetId}${version}`)

    // If version set and formState is falsey, try without the version
    if (version && !savedFormState) {
      savedFormState = storage.getItem(`FormState:${targetId}`)
    }

    // Process the field values to ensure we have coerced types
    if (savedFormState && savedFormState.hasOwnProperty('fields') && savedFormState.fields.length) {
      savedFormState.fields.forEach((fieldState, index) => {
        savedFormState.fields[index].value = coerceToPrimitiveType(savedFormState.fields[index].value)
      })
    }

    return savedFormState
  }

  /**
   * Save form state.
   *
   * @param {String} version Set a specific version value to save under
   */
  save (version = '') {
    // Ensure not saving while already saving
    if (this.status) {
      return
    }

    // Track the status of the saving process
    this.status = 1

    // Set a version in the name that will be stored
    if (version) {
      version = `:${version}`
    }

    // Get the form state
    let formState = this.getCurrentState()

    storage.setItem(`FormState:${formState.targetId}${version}`, formState, this.settings.storageType)

    // Turn off the status to allow other operations to work
    this.status = 0
  }

  /**
   * Restore form state.
   *
   * @param {String} version A specific version of the form's state to restore (only if it is available within the storage type)
   */
  restore (version = '') {
    // Ensure not restoring while already working
    if (this.status) {
      return
    }

    // Track the status of the restoring process
    this.status = 1

    // Set a version in the name that will be stored
    let formState = this.getSavedState(version)

    // Silently fade out into the night...
    if (!formState) {
      this.status = 0
      return
    }

    /**
     * Trigger a custom event before restoring the form's state
     *
     * @trigger FormState.restore:before
     * @param {Object} formState The form's state
     * @param {Object} options Extra data used when restoring the state
     */
    this.$target.trigger('FormState.restore:before', [formState, {
      FormState: this,
      version
    }])

    // Restore the fields within the form
    if (formState && formState.hasOwnProperty('fields')) {
      formState.fields.forEach((fieldState) => this.restoreField(fieldState))
    }

    /**
     * Trigger a custom event after restoring the form's state
     *
     * @trigger FormState.restore:after
     * @param {Object} formState The form's state
     * @param {Object} options Extra data used when restoring the state
     */
    this.$target.trigger('FormState.restore:after', [formState, {
      FormState: this,
      version
    }])

    // Turn off the status to allow other operations to work
    this.status = 0
  }

  /**
   * Restore a single field's state.
   *
   * @param {Object} fieldState
   */
  restoreField (fieldState) {
    let $field

    // Prioritise getting by ID
    if (fieldState.id) {
      $field = this.$target.find(`[id="${fieldState.id}"]`)
    } else {
      // Add in the type check if the tagName is input
      if (fieldState.tagName === 'input') {
        $field = this.$target.find(`${fieldState.tagName}[type="${fieldState.type}"][name="${fieldState.name}"]`)

      // Otherwise do what ya gotta do
      } else {
        $field = this.$target.find(`${fieldState.tagName}[name="${fieldState.name}"]`)
      }

      // Ensure only doing it for one field
      if ($field.length > 1) {
        $field = $field.filter(`:eq(${fieldState.nameIndex || 0})`)
      }
    }

    // Warn if can't find the field
    if (!$field.length) {
      console.warn('[LVL99 FormState] Field was not found in DOM to restore')
    }

    /**
     * Trigger a custom event before restoring the field's state
     *
     * @trigger FormState.restoreField:before
     * @param {Object} fieldState
     */
    $field.trigger('FormState.restoreField:before', [fieldState])

    // Set the field's value and any other information about its state
    switch (fieldState.tagName) {
      //
      // Input fields
      //
      case 'input':
        switch (fieldState.type) {
          // Radio and checkbox fields need to be checked if the value is not empty, or unchecked if it is
          case 'checkbox':
          case 'radio':
            if (fieldState.value !== '' && fieldState.value !== undefined && fieldState.value !== null) {
              $field.prop('checked', true)
            } else {
              $field.prop('checked', false)
            }
            break

          // This should work for most of them
          default:
            $field.val(fieldState.value)
            break
        }
        break

      // Textarea fields
      case 'textarea':
        $field.val(fieldState.value)
        break

      // Select fields
      case 'select':
        $field.find('option').each((index, option) => {
          let $option = $(option)

          // Yes linter, I do want to use ==
          if ($option.val() == fieldState.value) {
            $option.prop('selected', true)
          } else {
            $option.prop('selected', false)
          }
        })
        break
    }

    /**
     * Trigger a custom event after restoring the field's state
     *
     * @trigger FormState.restoreField:after
     * @param {Object} fieldState
     */
    $field.trigger('FormState.restoreField:after', [fieldState])

    // Trigger the field has changed
    $field.trigger('change')
  }

  /**
   * Clear form state from storage.
   *
   * @param {String} version A specific version of the form's state to clear
   */
  clear (version = '') {
    // Ensure not clearing while something is happening
    if (this.status) {
      return
    }

    // Track the status of the process
    this.status = 1

    // Set a version in the name that will be cleared
    if (version) {
      version = `:${version}`
    }

    // Check for the saved form state
    let targetId = this.$target.attr('id')
    storage.removeItem(`FormState:${targetId}${version}`)

    /**
     * Trigger a custom event after clearing the form's state
     *
     * @trigger FormState.clear:after
     * @param {Object} formState The form's state
     * @param {Object} options Extra data used when clearing the state
     */
    this.$target.trigger('FormState.clear:after', [formState, {
      FormState: this,
      version
    }])

    // Turn off the status to allow other operations to work
    this.status = 0
  }
}
<commit_msg>some form state refactors and trying to figure out docs<commit_after>/**
 * LVL99 Form State
 *
 * Use local or session storage to save a form's state. This means if a user navigates away that their data can be
 * stored and they can choose to resume or reset the form's state.
 *
 * @module lvl99/tools/FormState
 * @requires module:jquery
 * @requires module:lodash.merge
 * @requires module:lvl99/utils/parse
 * @requires module:lvl99/tools/Storage
 * @namespace lvl99.tools.FormState
 */

import merge from 'lodash.merge'
import { $ } from '../common'
import { coerceToPrimitiveType } from '../utils/parse'
import Storage, { LOCAL_STORAGE } from './storage'

/**
 * The various work states of the FormState instance.
 *
 * @typedef {Number} FormStateStatus
 * @readonly
 * @enum {FormStateStatus}
 */
const FORMSTATE_STATUS = {
  /** Instance is currently idle. */
  IDLE: 0,
  /** Instance is processing. */
  PROCESSING: 1
}

// Use the Storage tool to save the form's state
const STORAGE = new Storage()

/**
 * Get a field's current state.
 *
 * @static
 * @param {String|HTMLElement|jQueryObject} targetField
 * @returns {undefined|FieldStateSnapshot}
 */
export function getFieldState (targetField) {
  let $field = $(targetField)
  let tagName = $field[0].tagName.toLowerCase()
  let id = $field.attr('id')
  let type = $field.attr('type')
  let name = $field.attr('name')
  let $relatedFields = $field.parents('form').find(`[name="${name}"]`)
  let nameIndex = $relatedFields.index($field)
  let value = $field.val()

  /**
   * A field's state describing how to locate the field and its value at the time of saving.
   *
   * @typedef {Object} FieldStateSnapshot
   * @prop {Number} snapshot - Unix time in milliseconds of when the snapshot was taken
   * @prop {String} tagName - The field's HTML tag name
   * @prop {undefined|String} [id=undefined] - The field's `id` attribute value
   * @prop {undefined|String} [type=undefined] - The field's `type` attribute value
   * @prop {undefined|String} [name=undefined] - The field's `name` attribute value
   * @prop {Number} nameIndex - The field's numbered index for other fields that share the same name (e.g. radio buttons)
   * @prop {*} value - The field's computed value
   */
  let fieldState = {
    snapshot: Date.now(),
    tagName,
    id,
    type,
    name,
    nameIndex,
    value
  }

  // Extra transforms on the value depending on the type of field and its state
  switch (type) {
    // If a checkbox is not checked, it's state value should be ''
    case 'checkbox':
      if (!$field.is(':checked')) {
        fieldState.value = ''
      }
      break

    // Ignore checkboxes which haven't been checked
    case 'radio':
      if (!$field.is(':checked')) {
        return undefined
      }
  }

  /**
   * Trigger a custom event on the field to allow other things to manipulate the field's returned state.
   *
   * @trigger FormState.getFieldState:after
   * @param {FieldStateSnapshot} fieldState The generated field's state
   * @param {Object} options Extra data used to generate the state
   */
  $field.trigger('FormState.getFieldState:after', [fieldState, {
    $field,
    $relatedFields
  }])

  return fieldState
}

/**
 * FormState class.
 *
 * @namespace FormState
 * @class
 */
export default class FormState {
  /**
   * FormState constructor.
   *
   * @constructor
   * @param {String|HTMLElement|jQueryObject} target
   * @param {Object} options
   * @throws Error
   */
  constructor (target, options) {
    /**
     * The options for the FormState instance.
     *
     * @typedef {Object} FormStateOptions
     * @prop {String|HTMLElement|jQueryObject} target - The target form element.
     * @prop {String} storageType - The default storage type to use
     * @prop {Boolean} clearOnSubmit=true - Clear the form's state when submitted
     * @prop {String} includeFields - Selector of fields to save state values of.
     *
     * Defaults to HTML elements `input`, `select` and `textarea` which have a name attribute set, and any element
     * that returns a `jQuery.fn.val()` result with the attribute `data-form-state-include`.
     *
     * @prop {String} ignoreFields - Selector of fields to ignore values of when saving state.
     *
     * Defaults to ignoring input elements with type of button, submit, reset and image; any elements with the
     * attribute `data-form-state-ignore`; and any hidden input elements which have a name that starts with an
     * underscore (like WP nonce hidden inputs).
     *
     * Please note that password and file input elements will always be ignored.
     */
    this.settings = merge({
      target: target,
      storageType: LOCAL_STORAGE,
      clearOnSubmit: true,
      includeFields: 'input[name], select[name], textarea[name], [data-form-state-include]',
      ignoreFields: 'input[type="submit"], input[type="reset"], input[type="button"], input[type="image"], input[type="hidden"][name^="_"], [data-form-state-ignore]'
    }, options)

    // Error if falsey target given
    if (!this.settings.target) {
      throw new Error('Invalid target form element given')
    }

    /**
     * The element which represents the target form.
     *
     * @memberof FormState
     * @prop {jQueryObject} $target
     */
    this.$target = $(this.settings.target).first()

    // Error if no target element found in DOM
    if (!this.$target.length) {
      throw new Error('Target form element is not found')
    }

    // Error if invalid element or no ID set
    if (!this.$target.is('form') || !this.$target.attr('id')) {
      throw new Error('Target form element must be a form and have an ID set')
    }

    /**
     * @memberof FormState
     * @prop {FormStateStatus} status - The current working status of the FormState instance.
     */
    this.status = 0

    //
    // Attach event listeners to the target form
    //

    // Change = save
    this.$target.on('change', () => {
      this.save()
    })

    // Reset = restore
    this.$target.on('reset', () => {
      this.restore()
    })

    // Submit = clear (if settings are configured)
    this.$target.on('submit', () => {
      if (this.settings.clearOnSubmit) {
        this.clear()
      }
    })

    // After instantiation, restore the form state
    this.restore()
  }

  /**
   * Get the target's supported fields as a jQuery object.
   *
   * @returns {jQueryObject}
   */
  getFields () {
    return this.$target
      // Get only the included fields
      .find(this.settings.includeFields)
      // Remove any ignored fields
      .not(this.settings.ignoreFields)
      // Remove fields which are security issue or incompatible with local storage
      .not('[type="password"], [type="file"]')
  }

  /**
   * Get a field's current state.
   *
   * @param {String|HTMLElement|jQueryObject} targetField
   * @returns {undefined|FieldStateSnapshot}
   */
  getFieldState (targetField) {
    return getFieldState(targetField)
  }

  /**
   * Restore a single field's state.
   *
   * @param {FormFieldState} fieldState
   */
  restoreFieldState (fieldState) {
    let $field

    // Prioritise getting by ID
    if (fieldState.id) {
      $field = this.$target.find(`[id="${fieldState.id}"]`)
    } else {
      // Add in the type check if the tagName is input
      if (fieldState.tagName === 'input') {
        $field = this.$target.find(`${fieldState.tagName}[type="${fieldState.type}"][name="${fieldState.name}"]`)

        // Otherwise do what ya gotta do
      } else {
        $field = this.$target.find(`${fieldState.tagName}[name="${fieldState.name}"]`)
      }

      // Ensure only doing it for one field
      if ($field.length > 1) {
        $field = $field.filter(`:eq(${fieldState.nameIndex || 0})`)
      }
    }

    // Warn if can't find the field
    if (!$field.length) {
      console.warn('[FormState] Field was not found in DOM to restore')
    }

    /**
     * Trigger a custom event before restoring the field's state
     *
     * @trigger FormState.restoreField:before
     * @param {FieldStateSnapshot} fieldState
     */
    $field.trigger('FormState.restoreField:before', [fieldState])

    // Set the field's value and any other information about its state
    switch (fieldState.tagName) {
      //
      // Input fields
      //
      case 'input':
        switch (fieldState.type) {
          // Radio and checkbox fields need to be checked if the value is not empty, or unchecked if it is
          case 'checkbox':
          case 'radio':
            if (fieldState.value !== '' && fieldState.value !== undefined && fieldState.value !== null) {
              $field.prop('checked', true)
            } else {
              $field.prop('checked', false)
            }
            break

          // This should work for most of them
          default:
            $field.val(fieldState.value)
            break
        }
        break

      // Textarea fields
      case 'textarea':
        $field.val(fieldState.value)
        break

      // Select fields
      case 'select':
        $field.find('option').each((index, option) => {
          let $option = $(option)

          // Yes linter, I do want to use ==
          if ($option.val() == fieldState.value) {
            $option.prop('selected', true)
          } else {
            $option.prop('selected', false)
          }
        })
        break
    }

    /**
     * Trigger a custom event after restoring the field's state
     *
     * @trigger FormState.restoreField:after
     * @param {FieldStateSnapshot} fieldState
     */
    $field.trigger('FormState.restoreField:after', [fieldState])

    // Trigger the field has changed
    $field.trigger('change')
  }

  /**
   * Get a snapshot of the form's current state.
   *
   * @returns {FormStateSnapshot}
   */
  getCurrentState () {
    /**
     * The saved state of a form and its related fields.
     *
     * @typedef {Object} FormStateSnapshot
     * @prop {Number} snapshot - The milliseconds representing the time the snapshot was taken.
     * @prop {String} targetId - The form's ID attribute value.
     * @prop {FieldStateSnapshot[]} fields - An array of the form's field snapshots.
     */
    let formState = {
      snapshot: Date.now(),
      targetId: this.$target.attr('id'),
      fields: []
    }

    // Process all the form fields to extract the data
    let $fields = this.getFields()
    $fields.each((index, elem) => {
      let fieldState = this.getField(elem)
      if (fieldState) {
        formState.fields.push(fieldState)
      }
    })

    return formState
  }

  /**
   * Get a previously saved snapshot of the form's state.
   *
   * @param {String} version
   * @returns {FormStateSnapshot}
   */
  getSavedState (version = '') {
    // Set a version in the name that will be stored
    if (version) {
      version = `:${version}`
    }

    // Check for the saved form state
    let targetId = this.$target.attr('id')
    let savedFormState = STORAGE.getItem(`FormState:${targetId}${version}`, this.settings.storageType)

    // If version set and formState is falsey, try without the version
    if (version && !savedFormState) {
      savedFormState = STORAGE.getItem(`FormState:${targetId}`, this.settings.storageType)
    }

    // Process the field values to ensure we have coerced types
    if (savedFormState && savedFormState.hasOwnProperty('fields') && savedFormState.fields.length) {
      savedFormState.fields.forEach((fieldState, index) => {
        savedFormState.fields[index].value = coerceToPrimitiveType(savedFormState.fields[index].value)
      })
    }

    return savedFormState
  }

  /**
   * Save form state.
   *
   * @param {String} [version] - Set a specific version to save to.
   */
  save (version = '') {
    // Ensure not saving while already saving
    if (this.status) {
      return
    }

    // Track the status of the saving process
    this.status = FORMSTATE_STATUS.PROCESSING

    // Set a version in the name that will be stored
    if (version) {
      version = `:${version}`
    }

    // Get and save the form state
    let formState = this.getCurrentState()
    STORAGE.setItem(`FormState:${formState.targetId}${version}`, formState, this.settings.storageType)

    // Turn off the status to allow other operations to work
    this.status = FORMSTATE_STATUS.IDLE
  }

  /**
   * Restore form state.
   *
   * @param {String} [version] - A specific version of the form's state to restore (only if it is available within the storage type).
   */
  restore (version = '') {
    // Ensure not restoring while already working
    if (this.status) {
      return
    }

    // Track the status of the restoring process
    this.status = FORMSTATE_STATUS.PROCESSING

    // Set a version in the name that will be stored
    let formState = this.getSavedState(version)

    // Silently fade out into the night...
    if (!formState) {
      this.status = FORMSTATE_STATUS.IDLE
      return
    }

    /**
     * Trigger a custom event before restoring the form's state.
     *
     * @trigger FormState.restore:before
     * @param {FormStateSnapshot} formState - The form's state to restore to.
     * @param {Object} options - Extra data used when restoring the state.
     * @param {FormState} options.FormState - The current FormState instance firing the event.
     * @param {String} options.version - The version of the form's state being restored.
     */
    this.$target.trigger('FormState.restore:before', [formState, {
      FormState: this,
      version
    }])

    // Restore the fields within the form
    if (formState && formState.hasOwnProperty('fields')) {
      formState.fields.forEach((fieldState) => this.restoreField(fieldState))
    }

    /**
     * Trigger a custom event after restoring the form's state.
     *
     * @trigger FormState.restore:after
     * @param {FormStateSnapshot} formState - The form's state to restore to.
     * @param {Object} options - Extra data used when restoring the state.
     * @param {FormState} options.FormState - The current FormState instance firing the event.
     * @param {String} options.version - The version of the form's state being restored.
     */
    this.$target.trigger('FormState.restore:after', [formState, {
      FormState: this,
      version
    }])

    // Turn off the status to allow other operations to work
    this.status = FORMSTATE_STATUS.IDLE
  }

  /**
   * Clear form state from storage.
   *
   * @param {String} [version] - A specific version of the form's state to clear.
   */
  clear (version = '') {
    // Ensure not clearing while something is happening
    if (this.status) {
      return
    }

    // Track the status of the process
    this.status = FORMSTATE_STATUS.PROCESSING

    // Set a version in the name that will be cleared
    if (version) {
      version = `:${version}`
    }

    // Check for the saved form state
    let targetId = this.$target.attr('id')
    storage.removeItem(`FormState:${targetId}${version}`, this.settings.storageType)

    /**
     * Trigger a custom event after clearing the form's state.
     *
     * @trigger FormState.clear:after
     * @param {Object} options - Extra data used when restoring the state.
     * @param {FormState} options.FormState - The current FormState instance firing the event.
     * @param {String} options.version - The version of the form's state being restored.
     */
    this.$target.trigger('FormState.clear:after', [{
      FormState: this,
      version
    }])

    // Turn off the status to allow other operations to work
    this.status = FORMSTATE_STATUS.IDLE
  }
}
<|endoftext|>


<commit_before>var gulp    = require('gulp'),
    shell   = require('gulp-shell'),
    os      = require('os');
    gulpZip = require('gulp-zip'),
    conf    = require('./import-conf');

/* START: change these paths */
var bi_server_command_path  = conf.bi_server_command_path
    petaho_URL              = conf.petaho_URL,
    pentaho_username        = conf.pentaho_username,
    pentaho_password        = conf.pentaho_password;
/* END: change these paths */

// replace command for windows os
if(os.platform() === 'win32') {
    bi_server_command_path = bi_server_command_path.replace('.sh', '.bat');
}

var pentaho_import  = bi_server_command_path
                      + ' --import'
                      + ' --url=' + petaho_URL
                      + ' --username=' + pentaho_username
                      + ' --password=' + pentaho_password
                      + ' --overwrite=true --permission=true --retainOwnership=true',

    project_path      = conf.project_path,
    pentaho_path      = conf.pentaho_path,
    pentaho_dist_path = conf.pentaho_dist_path,
    zipfile_path      = conf.zipfile_path,

    // pentaho paths
    path = {
        bower   : pentaho_path,                             // bower_components
        src     : pentaho_path,                             // scripts/html
        styles  : pentaho_path + '/src/app',                // styles
        html    : pentaho_path + '/src',                    // html (new files)
        cda     : pentaho_path + '/cdas',                   // cda files
        dist    : pentaho_dist_path,                        // dist

        testHtml: pentaho_path + '/src',                    // html (new files)
        test    : pentaho_path,                             // scripts/html
    };

// replace slash for windows os
if(os.platform() === 'win32') {
    zipfile_path = zipfile_path.replace(/\//g, '\\')
}

// zip file paths
var file_path = {
        bower   : zipfile_path + 'bower_components.zip',   // bower_components
        src     : zipfile_path + 'src.zip',                // scripts/html
        styles  : zipfile_path + 'css.zip',                // styles
        html    : zipfile_path + 'index.zip',              // html (new files)
        cda     : zipfile_path + 'cda.zip',                // cda files
        dist    : zipfile_path + 'dist.zip',               // dist

        testHtml: zipfile_path + 'test.zip',               // html (new files)
        test    : zipfile_path + 'specs.zip',              // specs
    };

/* SOURCE TASKS
=================================== */

gulp.task('zip:bower', function() {
    return gulp.src('./src/bower_components/**', {
            base: './'
        })
        .pipe(gulpZip('bower_components.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import:bower', ['zip:bower'], shell.task([
    pentaho_import + ' --path=' + path.bower + ' --file-path=' + file_path.bower
]));

// *.js and *.html
gulp.task('zip:src', function() {
    return gulp.src('./src/app/**', {
            base: './'
        })
        .pipe(gulpZip('src.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src', ['zip:src'], shell.task([
    pentaho_import + ' --path=' + path.src + ' --file-path=' + file_path.src
]));

// *.scss -> index.css
gulp.task('zip:src-style', function() {
    return gulp.src('./.tmp/serve/app/**')
        .pipe(gulpZip('css.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src-styles', ['zip:src-style'], shell.task([
    pentaho_import + ' --path=' + path.styles + ' --file-path=' + file_path.styles
]));

// index.html
gulp.task('zip:src-index', function() {
    return gulp.src(['./.tmp/serve/**', '!./.tmp/serve/{app,app/**}'])
        .pipe(gulpZip('index.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src-html', ['zip:src-index'], shell.task([
    pentaho_import + ' --path=' + path.html + ' --file-path=' + file_path.html
]));

// *.cda
gulp.task('zip:cda', function() {
    return gulp.src('./src/assets/cdas/**', {
            base: './src/assets/cdas'
        })
        .pipe(gulpZip('cda.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-cda', ['zip:cda'], shell.task([
    pentaho_import + ' --path=' + path.cda + ' --file-path=' + file_path.cda
]));

/* DIST TASKS
=================================== */

gulp.task('import:dist', ['clean'], function() {
    gulp.start('import-cda');
    gulp.start('build-dist');
});

gulp.task('build-dist', ['build'], function() {
    gulp.start('import-dist');
});

gulp.task('zip:dist', function() {
    return gulp.src(['./' + project_path + '/**', '!./' + project_path + '{/maps,/maps/**}'], {
            base: './'
        })
        .pipe(gulpZip('dist.zip'))
        .pipe(gulp.dest('./zip'));
});

gulp.task('import-dist', ['zip:dist'], shell.task([
    pentaho_import + ' --path=' + path.dist + ' --file-path=' + file_path.dist
]));

/* TESTS TASKS
=================================== */

// index.html
gulp.task('zip:test-html', function() {
    return gulp.src(['./.tmp/serve/test.html', './.tmp/serve/test.xcdf'])
        .pipe(gulpZip('test.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-test-html', ['zip:test-html'], shell.task([
    pentaho_import + ' --path=' + path.testHtml + ' --file-path=' + file_path.testHtml
]));

// *.spec.js
gulp.task('zip:test', function() {
    return gulp.src('./src/tests/jasmine/**', {
        base: './src/tests/jasmine'
    })
        .pipe(gulpZip('specs.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-test', ['zip:test'], shell.task([
    pentaho_import + ' --path=' + path.test + ' --file-path=' + file_path.test
]));
<commit_msg>add import:cda<commit_after>var gulp    = require('gulp'),
    shell   = require('gulp-shell'),
    os      = require('os');
    gulpZip = require('gulp-zip'),
    conf    = require('./import-conf');

/* START: change these paths */
var bi_server_command_path  = conf.bi_server_command_path
    petaho_URL              = conf.petaho_URL,
    pentaho_username        = conf.pentaho_username,
    pentaho_password        = conf.pentaho_password;
/* END: change these paths */

// replace command for windows os
if(os.platform() === 'win32') {
    bi_server_command_path = bi_server_command_path.replace('.sh', '.bat');
}

var pentaho_import  = bi_server_command_path
                      + ' --import'
                      + ' --url=' + petaho_URL
                      + ' --username=' + pentaho_username
                      + ' --password=' + pentaho_password
                      + ' --overwrite=true --permission=true --retainOwnership=true',

    project_path      = conf.project_path,
    pentaho_path      = conf.pentaho_path,
    pentaho_dist_path = conf.pentaho_dist_path,
    zipfile_path      = conf.zipfile_path,

    // pentaho paths
    path = {
        bower   : pentaho_path,                             // bower_components
        src     : pentaho_path,                             // scripts/html
        styles  : pentaho_path + '/src/app',                // styles
        html    : pentaho_path + '/src',                    // html (new files)
        cda     : pentaho_path + '/cdas',                   // cda files
        dist    : pentaho_dist_path,                        // dist

        testHtml: pentaho_path + '/src',                    // html (new files)
        test    : pentaho_path,                             // scripts/html
    };

// replace slash for windows os
if(os.platform() === 'win32') {
    zipfile_path = zipfile_path.replace(/\//g, '\\')
}

// zip file paths
var file_path = {
        bower   : zipfile_path + 'bower_components.zip',   // bower_components
        src     : zipfile_path + 'src.zip',                // scripts/html
        styles  : zipfile_path + 'css.zip',                // styles
        html    : zipfile_path + 'index.zip',              // html (new files)
        cda     : zipfile_path + 'cda.zip',                // cda files
        dist    : zipfile_path + 'dist.zip',               // dist

        testHtml: zipfile_path + 'test.zip',               // html (new files)
        test    : zipfile_path + 'specs.zip',              // specs
    };

/* SOURCE TASKS
=================================== */

gulp.task('zip:bower', function() {
    return gulp.src('./src/bower_components/**', {
            base: './'
        })
        .pipe(gulpZip('bower_components.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import:bower', ['zip:bower'], shell.task([
    pentaho_import + ' --path=' + path.bower + ' --file-path=' + file_path.bower
]));

// *.js and *.html
gulp.task('zip:src', function() {
    return gulp.src('./src/app/**', {
            base: './'
        })
        .pipe(gulpZip('src.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src', ['zip:src'], shell.task([
    pentaho_import + ' --path=' + path.src + ' --file-path=' + file_path.src
]));

// *.scss -> index.css
gulp.task('zip:src-style', function() {
    return gulp.src('./.tmp/serve/app/**')
        .pipe(gulpZip('css.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src-styles', ['zip:src-style'], shell.task([
    pentaho_import + ' --path=' + path.styles + ' --file-path=' + file_path.styles
]));

// index.html
gulp.task('zip:src-index', function() {
    return gulp.src(['./.tmp/serve/**', '!./.tmp/serve/{app,app/**}'])
        .pipe(gulpZip('index.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-src-html', ['zip:src-index'], shell.task([
    pentaho_import + ' --path=' + path.html + ' --file-path=' + file_path.html
]));

// *.cda
gulp.task('zip:cda', function() {
    return gulp.src('./src/assets/cdas/**', {
            base: './src/assets/cdas'
        })
        .pipe(gulpZip('cda.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-cda', ['zip:cda'], shell.task([
    pentaho_import + ' --path=' + path.cda + ' --file-path=' + file_path.cda
]));

gulp.task('import:cda', function() {
    gulp.start('import-cda');
});

/* DIST TASKS
=================================== */

gulp.task('import:dist', ['clean'], function() {
    gulp.start('import-cda');
    gulp.start('build-dist');
});

gulp.task('build-dist', ['build'], function() {
    gulp.start('import-dist');
});

gulp.task('zip:dist', function() {
    return gulp.src(['./' + project_path + '/**', '!./' + project_path + '{/maps,/maps/**}'], {
            base: './'
        })
        .pipe(gulpZip('dist.zip'))
        .pipe(gulp.dest('./zip'));
});

gulp.task('import-dist', ['zip:dist'], shell.task([
    pentaho_import + ' --path=' + path.dist + ' --file-path=' + file_path.dist
]));

/* TESTS TASKS
=================================== */

// index.html
gulp.task('zip:test-html', function() {
    return gulp.src(['./.tmp/serve/test.html', './.tmp/serve/test.xcdf'])
        .pipe(gulpZip('test.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-test-html', ['zip:test-html'], shell.task([
    pentaho_import + ' --path=' + path.testHtml + ' --file-path=' + file_path.testHtml
]));

// *.spec.js
gulp.task('zip:test', function() {
    return gulp.src('./src/tests/jasmine/**', {
        base: './src/tests/jasmine'
    })
        .pipe(gulpZip('specs.zip'))
        .pipe(gulp.dest('./zip'));
});
gulp.task('import-test', ['zip:test'], shell.task([
    pentaho_import + ' --path=' + path.test + ' --file-path=' + file_path.test
]));
<|endoftext|>


<commit_before>// Creates N.mailer interface. Currently it allows to send email letters only.

'use strict';

var _          = require('lodash');
var os         = require('os');
var nodemailer = require('nodemailer');

// List of global settings used by the mailer.
var SETTINGS = [
  'email_transport'
, 'email_header_from'
];

// Map of available internal transport types to NodeMailer transport engines.
var TRANSPORTS = {
  sendmail: 'SENDMAIL'
, dummy:    'STUB'
};

module.exports = function (N) {
  N.wire.before('init:server', function init_mailer() {
    var settings, transport, logger;

    // Initialize or update mailer context if email transport setting was changed.
    //
    function checkTransport(callback) {
      N.settings.get(SETTINGS, {}, function (err, fetched) {
        if (err) {
          callback(err);
          return;
        }

        if (!_.has(TRANSPORTS, fetched.email_transport)) {
          callback('Unknown email transport "' + fetched.email_transport + '"');
          return;
        }

        // Reinitialize if any of email settings was changed.
        if (!_.isEqual(settings, fetched)) {
          settings  = fetched;

          transport = nodemailer.createTransport(TRANSPORTS[settings.email_transport]);
          logger    = N.logger.getLogger('system@mailer.' + settings.email_transport);
        }

        callback();
      });
    }

    // Add a letter to the mailer's queue and call back when that's actually sent.
    //
    function send(options, callback) {
      checkTransport(function (err) {
        if (err) {
          callback(err);
          return;
        }

        // Fill-in default letter options.
        options = _.assign({}, options, { from: settings.email_header_from });

        transport.sendMail(options, function (err, result) {
          if (err) {
            logger.error('Mailer transport error, can\'t send letter to %j', options.to);
            callback(err);
            return;
          }

          logger.debug('Sent a letter with content:%s%s', os.EOL, result.message);
          callback();
        });
      });
    }

    // Expose the public interface.
    N.mailer = { send: send };
  });
};
<commit_msg>Mailer: updated notemailer calls to new api<commit_after>// Creates N.mailer interface. Currently it allows to send email letters only.

'use strict';

var _          = require('lodash');
var os         = require('os');
var nodemailer = require('nodemailer');
var util       = require('util');

// List of global settings used by the mailer.
var SETTINGS = [
  'email_transport'
, 'email_header_from'
];

// Map of available internal transport types to NodeMailer transport engines.
var TRANSPORTS = {
  sendmail: function () {
    return require('nodemailer-sendmail-transport')();
  },
  dummy: function () {
    return require('nodemailer-stub-transport')();
  }
};

module.exports = function (N) {
  N.wire.before('init:server', function init_mailer() {
    var settings, transport, logger;

    // Initialize or update mailer context if email transport setting was changed.
    //
    function checkTransport(callback) {
      N.settings.get(SETTINGS, {}, function (err, fetched) {
        if (err) {
          callback(err);
          return;
        }

        if (!_.has(TRANSPORTS, fetched.email_transport)) {
          callback('Unknown email transport "' + fetched.email_transport + '"');
          return;
        }

        // Reinitialize if any of email settings was changed.
        if (!_.isEqual(settings, fetched)) {
          settings  = fetched;

          transport = nodemailer.createTransport(TRANSPORTS[settings.email_transport]());
          logger    = N.logger.getLogger('system@mailer.' + settings.email_transport);
        }

        callback();
      });
    }

    // Add a letter to the mailer's queue and call back when that's actually sent.
    //
    function send(options, callback) {
      checkTransport(function (err) {
        if (err) {
          callback(err);
          return;
        }

        // Fill-in default letter options.
        options = _.assign({}, options, { from: settings.email_header_from });

        transport.sendMail(options, function (err, info) {
          if (err) {
            logger.error('Mailer error: %s%s%s%s', os.EOL, util.inspect(err), os.EOL, util.inspect(options));
            callback(err);
            return;
          }

          logger.debug('Mailer ok: %s%s', os.EOL, util.inspect(options));
          callback();
        });
      });
    }

    // Expose the public interface.
    N.mailer = { send: send };
  });
};
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { ListItem, ListItemSecondaryAction } from 'material-ui/List';
import TextField from 'material-ui/TextField';
import IconButton from 'material-ui/IconButton';
import Cancel from 'material-ui-icons/Cancel';
import Save from 'material-ui-icons/Save';

class CategoryEditor extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.category
    };
  }

  render() {
    const inputProps = {
      "aria-label": this.props.ariaLabel
    }
    return (
      <ListItem divider>
        <TextField
          id="categoryeditor"
          inputProps={inputProps}
          value={this.state.value}
          autoFocus
          margin="normal"
        />
        <ListItemSecondaryAction>
          <IconButton aria-label="Save">
            <Save />
          </IconButton>
          <IconButton aria-label="Cancel" onClick={this.props.handleCancel}>
            <Cancel />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    );
  }
}

CategoryEditor.propTypes = {
  category: PropTypes.string,
  url: PropTypes.string,
  ariaLabel: PropTypes.string,
  // handleCancel(e)
  handleCancel: PropTypes.func.isRequired
  // saveHandler(e, category, url)
  // saveHandler: PropTypes.func.isRequired
}

CategoryEditor.defaultProps = {
  category: "",
  url: ""
}

export default CategoryEditor;
<commit_msg>category editor controlled component<commit_after>import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { ListItem, ListItemSecondaryAction } from 'material-ui/List';
import TextField from 'material-ui/TextField';
import IconButton from 'material-ui/IconButton';
import Cancel from 'material-ui-icons/Cancel';
import Save from 'material-ui-icons/Save';

class CategoryEditor extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.category
    };
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.setState({value: e.target.value})
  }

  render() {
    const inputProps = {
      "aria-label": this.props.ariaLabel
    }
    return (
      <ListItem divider>
        <TextField
          id="categoryeditor"
          inputProps={inputProps}
          value={this.state.value}
          onChange={this.handleChange}
          autoFocus
          margin="normal"
        />
        <ListItemSecondaryAction>
          <IconButton aria-label="Save">
            <Save />
          </IconButton>
          <IconButton aria-label="Cancel" onClick={this.props.handleCancel}>
            <Cancel />
          </IconButton>
        </ListItemSecondaryAction>
      </ListItem>
    );
  }
}

CategoryEditor.propTypes = {
  category: PropTypes.string,
  url: PropTypes.string,
  ariaLabel: PropTypes.string,
  // handleCancel(e)
  handleCancel: PropTypes.func.isRequired
  // saveHandler(e, category, url)
  // saveHandler: PropTypes.func.isRequired
}

CategoryEditor.defaultProps = {
  category: "",
  url: ""
}

export default CategoryEditor;
<|endoftext|>


<commit_before>module.exports = {
  framework: 'mocha',
  test_page: 'tests/index.html?hidepassed',
  disable_watching: true,
  launch_in_ci: [
    'Chrome'
  ],
  launch_in_dev: [
    'Chrome'
  ],
  browser_args: {
    Chrome: {
      mode: 'ci',
      args: [
        '--disable-gpu',
        '--headless',
        '--remote-debugging-port=0',
        '--window-size=1440,900'
      ]
    },
  }
};
<commit_msg>testem: Use `--no-sandbox` on TravisCI<commit_after>/* eslint-env node */

module.exports = {
  framework: 'mocha',
  test_page: 'tests/index.html?hidepassed',
  disable_watching: true,
  launch_in_ci: [
    'Chrome'
  ],
  launch_in_dev: [
    'Chrome'
  ],
  browser_args: {
    Chrome: {
      mode: 'ci',
      args: [
        // --no-sandbox is needed when running Chrome inside a container
        process.env.TRAVIS ? '--no-sandbox' : null,

        '--disable-gpu',
        '--headless',
        '--remote-debugging-port=0',
        '--window-size=1440,900'
      ].filter(Boolean)
    },
  }
};
<|endoftext|>


<commit_before>/**
 * Update Checking Unit
 *
 * Makes a request to Ghost.org to request release & custom notifications.
 * The service is provided in return for users opting in to anonymous usage data collection.
 *
 * Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
 */

const crypto = require('crypto');

const exec = require('child_process').exec;
const moment = require('moment');
const Promise = require('bluebird');
const _ = require('lodash');
const url = require('url');
const debug = require('ghost-ignition').debug('update-check');
const api = require('./api').v3;
const GhostMailer = require('./services/mail').GhostMailer;
const config = require('../shared/config');
const urlUtils = require('./../shared/url-utils');
const errors = require('@tryghost/errors');
const i18n = require('../shared/i18n');
const logging = require('../shared/logging');
const request = require('./lib/request');
const ghostVersion = require('./lib/ghost-version');

const internal = {context: {internal: true}};
const allowedCheckEnvironments = ['development', 'production'];

const ghostMailer = new GhostMailer();

function nextCheckTimestamp() {
    const now = Math.round(new Date().getTime() / 1000);
    return now + (24 * 3600);
}

/**
 * @description Centralised error handler for the update check unit.
 *
 * CASES:
 *   - the update check service returns an error
 *   - error during collecting blog stats
 *
 * We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
 * update checks will happen.
 *
 * @param err
 */
function updateCheckError(err) {
    api.settings.edit({
        settings: [{
            key: 'next_update_check',
            value: nextCheckTimestamp()
        }]
    }, internal);

    err.context = i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
    err.help = i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});
    logging.error(err);
}

/**
 * @description Create a Ghost notification and call the API controller.
 *
 * @param {Object} notification
 * @return {Promise}
 */
async function createCustomNotification(notification) {
    if (!notification) {
        return;
    }

    const {users} = await api.users.browse(Object.assign({
        limit: 'all',
        include: ['roles']
    }, internal));

    const adminEmails = users
        .filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
        .map(user => user.email);

    const siteUrl = config.get('url');

    for (const message of notification.messages) {
        const toAdd = {
            // @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
            custom: !!notification.custom,
            createdAt: moment(notification.created_at).toDate(),
            status: message.status || 'alert',
            type: message.type || 'info',
            id: message.id,
            dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
            top: !!message.top,
            message: message.content
        };

        if (toAdd.type === 'alert') {
            for (const email of adminEmails) {
                try {
                    ghostMailer.send({
                        to: email,
                        subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
                        html: toAdd.message,
                        forceTextContent: true
                    });
                } catch (err) {
                    logging.err(err);
                }
            }
        }

        debug('Add Custom Notification', toAdd);
        await api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
    }
}

/**
 * @description Collect stats from your blog.
 * @returns {Promise}
 */
function updateCheckData() {
    let data = {};
    let mailConfig = config.get('mail');

    data.ghost_version = ghostVersion.original;
    data.node_version = process.versions.node;
    data.env = config.get('env');
    data.database_type = config.get('database').client;
    data.email_transport = mailConfig &&
        (mailConfig.options && mailConfig.options.service ?
            mailConfig.options.service :
            mailConfig.transport);

    return Promise.props({
        hash: api.settings.read(_.extend({key: 'db_hash'}, internal)).reflect(),
        theme: api.settings.read(_.extend({key: 'active_theme'}, internal)).reflect(),
        posts: api.posts.browse().reflect(),
        users: api.users.browse(internal).reflect(),
        npm: Promise.promisify(exec)('npm -v').reflect()
    }).then(function (descriptors) {
        const hash = descriptors.hash.value().settings[0];
        const theme = descriptors.theme.value().settings[0];
        const posts = descriptors.posts.value();
        const users = descriptors.users.value();
        const npm = descriptors.npm.value();
        const blogUrl = urlUtils.urlFor('home', true);
        const parsedBlogUrl = url.parse(blogUrl);
        const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;

        data.url = blogUrl;
        data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
        data.theme = theme ? theme.value : '';
        data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
        data.user_count = users && users.users && users.users.length ? users.users.length : 0;
        data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
        data.npm_version = npm.trim();

        return data;
    }).catch(updateCheckError);
}

/**
 * @description Perform request to update check service.
 *
 * With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
 * service. Enabled or disabled, you will receive the latest notification available from the service.
 *
 * @see https://ghost.org/docs/concepts/config/#privacy
 * @returns {Promise}
 */
function updateCheckRequest() {
    return updateCheckData()
        .then(function then(reqData) {
            let reqObj = {
                timeout: 1000,
                headers: {}
            };

            let checkEndpoint = config.get('updateCheck:url');
            let checkMethod = config.isPrivacyDisabled('useUpdateCheck') ? 'GET' : 'POST';

            // CASE: Expose stats and do a check-in
            if (checkMethod === 'POST') {
                reqObj.json = true;
                reqObj.body = reqData;
                reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
                reqObj.headers['Content-Type'] = 'application/json';
            } else {
                reqObj.json = true;
                reqObj.query = {
                    ghost_version: reqData.ghost_version
                };
            }

            debug('Request Update Check Service', checkEndpoint);

            return request(checkEndpoint, reqObj)
                .then(function (response) {
                    return response.body;
                })
                .catch(function (err) {
                // CASE: no notifications available, ignore
                    if (err.statusCode === 404) {
                        return {
                            next_check: nextCheckTimestamp(),
                            notifications: []
                        };
                    }

                    // CASE: service returns JSON error, deserialize into JS error
                    if (err.response && err.response.body && typeof err.response.body === 'object') {
                        err = errors.utils.deserialize(err.response.body);
                    }

                    throw err;
                });
        });
}

/**
 * @description This function handles the response from the update check service.
 *
 * The helper does three things:
 *
 * 1. Updates the time in the settings table to know when we can execute the next update check request.
 * 2. Iterates over the received notifications and filters them out based on your notification groups.
 * 3. Calls a custom helper to generate a Ghost notification for the database.
 *
 * The structure of the response is:
 *
 * {
 *  id: 20,
 *  version: 'all4',
 *  messages:
 *     [{
 *          id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
 *          version: '^2',
 *          content: 'Hallouuuu custom',
 *          top: true,
 *          dismissible: true,
 *          type: 'info'
 *      }],
 *  created_at: '2021-10-06T07:00:00.000Z',
 *  custom: 1,
 *  next_check: 1555608722
 * }
 *
 *
 * Example for grouped custom notifications in config:
 *
 *  "notificationGroups": ["migration", "something"]
 *
 * The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
 *
 * @param {Object} response
 * @return {Promise}
 */
async function updateCheckResponse(response) {
    let notifications = [];
    let notificationGroups = (config.get('notificationGroups') || []).concat(['all']);

    debug('Notification Groups', notificationGroups);
    debug('Response Update Check Service', response);

    await api.settings.edit({
        settings: [{
            key: 'next_update_check',
            value: response.next_check
        }]
    }, internal);

    /**
     * @NOTE:
     *
     * When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
     * But we wanted to already add the support for future functionality.
     * That's why this helper supports two ways: returning an array of messages or returning an object with
     * a "notifications" key. The second one is probably the best, because we need to support "next_check"
     * on the root level of the response.
     */
    if (_.isArray(response)) {
        notifications = response;
    } else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
        notifications = response.notifications;
    } else {
        // CASE: default right now
        notifications = [response];
    }

    // CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
    if (notificationGroups.length) {
        notifications = notifications.filter(function (notification) {
            // CASE: release notification, keep
            if (!notification.custom) {
                return true;
            }

            // CASE: filter out messages based on your groups
            return _.includes(notificationGroups.map(function (groupIdentifier) {
                if (notification.version.match(new RegExp(groupIdentifier))) {
                    return true;
                }

                return false;
            }), true) === true;
        });
    }

    return Promise.each(notifications, createCustomNotification);
}

/**
 * @description Entry point to trigger the update check unit.
 *
 * Based on a settings value, we check if `next_update_check` is less than now to decide whether
 * we should request the update check service (http://updates.ghost.org) or not.
 *
 * @returns {Promise}
 */
async function updateCheck() {
    // CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
    if (_.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
        return;
    }

    const result = await api.settings.read(_.extend({key: 'next_update_check'}, internal));

    const nextUpdateCheck = result.settings[0];

    // CASE: Next update check should happen now?
    // @NOTE: You can skip this check by adding a config value. This is helpful for developing.
    if (!config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
        return;
    }

    try {
        const response = await updateCheckRequest();

        await updateCheckResponse(response);
    } catch (err) {
        updateCheckError(err);
    }
}

module.exports = updateCheck;
<commit_msg>Revert "Upgraded the update-check api to v4"<commit_after>/**
 * Update Checking Unit
 *
 * Makes a request to Ghost.org to request release & custom notifications.
 * The service is provided in return for users opting in to anonymous usage data collection.
 *
 * Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config file.
 */

const crypto = require('crypto');

const exec = require('child_process').exec;
const moment = require('moment');
const Promise = require('bluebird');
const _ = require('lodash');
const url = require('url');
const debug = require('ghost-ignition').debug('update-check');
const api = require('./api').v2;
const GhostMailer = require('./services/mail').GhostMailer;
const config = require('../shared/config');
const urlUtils = require('./../shared/url-utils');
const errors = require('@tryghost/errors');
const i18n = require('../shared/i18n');
const logging = require('../shared/logging');
const request = require('./lib/request');
const ghostVersion = require('./lib/ghost-version');

const internal = {context: {internal: true}};
const allowedCheckEnvironments = ['development', 'production'];

const ghostMailer = new GhostMailer();

function nextCheckTimestamp() {
    const now = Math.round(new Date().getTime() / 1000);
    return now + (24 * 3600);
}

/**
 * @description Centralised error handler for the update check unit.
 *
 * CASES:
 *   - the update check service returns an error
 *   - error during collecting blog stats
 *
 * We still need to ensure that we set the "next_update_check" to a new value, otherwise no more
 * update checks will happen.
 *
 * @param err
 */
function updateCheckError(err) {
    api.settings.edit({
        settings: [{
            key: 'next_update_check',
            value: nextCheckTimestamp()
        }]
    }, internal);

    err.context = i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
    err.help = i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://ghost.org/docs/'});
    logging.error(err);
}

/**
 * @description Create a Ghost notification and call the API controller.
 *
 * @param {Object} notification
 * @return {Promise}
 */
async function createCustomNotification(notification) {
    if (!notification) {
        return;
    }

    const {users} = await api.users.browse(Object.assign({
        limit: 'all',
        include: ['roles']
    }, internal));

    const adminEmails = users
        .filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
        .map(user => user.email);

    const siteUrl = config.get('url');

    for (const message of notification.messages) {
        const toAdd = {
            // @NOTE: the update check service returns "0" or "1" (https://github.com/TryGhost/UpdateCheck/issues/43)
            custom: !!notification.custom,
            createdAt: moment(notification.created_at).toDate(),
            status: message.status || 'alert',
            type: message.type || 'info',
            id: message.id,
            dismissible: Object.prototype.hasOwnProperty.call(message, 'dismissible') ? message.dismissible : true,
            top: !!message.top,
            message: message.content
        };

        if (toAdd.type === 'alert') {
            for (const email of adminEmails) {
                try {
                    ghostMailer.send({
                        to: email,
                        subject: `Action required: Critical alert from Ghost instance ${siteUrl}`,
                        html: toAdd.message,
                        forceTextContent: true
                    });
                } catch (err) {
                    logging.err(err);
                }
            }
        }

        debug('Add Custom Notification', toAdd);
        await api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
    }
}

/**
 * @description Collect stats from your blog.
 * @returns {Promise}
 */
function updateCheckData() {
    let data = {};
    let mailConfig = config.get('mail');

    data.ghost_version = ghostVersion.original;
    data.node_version = process.versions.node;
    data.env = config.get('env');
    data.database_type = config.get('database').client;
    data.email_transport = mailConfig &&
        (mailConfig.options && mailConfig.options.service ?
            mailConfig.options.service :
            mailConfig.transport);

    return Promise.props({
        hash: api.settings.read(_.extend({key: 'db_hash'}, internal)).reflect(),
        theme: api.settings.read(_.extend({key: 'active_theme'}, internal)).reflect(),
        posts: api.posts.browse().reflect(),
        users: api.users.browse(internal).reflect(),
        npm: Promise.promisify(exec)('npm -v').reflect()
    }).then(function (descriptors) {
        const hash = descriptors.hash.value().settings[0];
        const theme = descriptors.theme.value().settings[0];
        const posts = descriptors.posts.value();
        const users = descriptors.users.value();
        const npm = descriptors.npm.value();
        const blogUrl = urlUtils.urlFor('home', true);
        const parsedBlogUrl = url.parse(blogUrl);
        const blogId = parsedBlogUrl.hostname + parsedBlogUrl.pathname.replace(/\//, '') + hash.value;

        data.url = blogUrl;
        data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
        data.theme = theme ? theme.value : '';
        data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
        data.user_count = users && users.users && users.users.length ? users.users.length : 0;
        data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
        data.npm_version = npm.trim();

        return data;
    }).catch(updateCheckError);
}

/**
 * @description Perform request to update check service.
 *
 * With the privacy setting `useUpdateCheck` you can control if you want to expose data/stats from your blog to the
 * service. Enabled or disabled, you will receive the latest notification available from the service.
 *
 * @see https://ghost.org/docs/concepts/config/#privacy
 * @returns {Promise}
 */
function updateCheckRequest() {
    return updateCheckData()
        .then(function then(reqData) {
            let reqObj = {
                timeout: 1000,
                headers: {}
            };

            let checkEndpoint = config.get('updateCheck:url');
            let checkMethod = config.isPrivacyDisabled('useUpdateCheck') ? 'GET' : 'POST';

            // CASE: Expose stats and do a check-in
            if (checkMethod === 'POST') {
                reqObj.json = true;
                reqObj.body = reqData;
                reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
                reqObj.headers['Content-Type'] = 'application/json';
            } else {
                reqObj.json = true;
                reqObj.query = {
                    ghost_version: reqData.ghost_version
                };
            }

            debug('Request Update Check Service', checkEndpoint);

            return request(checkEndpoint, reqObj)
                .then(function (response) {
                    return response.body;
                })
                .catch(function (err) {
                // CASE: no notifications available, ignore
                    if (err.statusCode === 404) {
                        return {
                            next_check: nextCheckTimestamp(),
                            notifications: []
                        };
                    }

                    // CASE: service returns JSON error, deserialize into JS error
                    if (err.response && err.response.body && typeof err.response.body === 'object') {
                        err = errors.utils.deserialize(err.response.body);
                    }

                    throw err;
                });
        });
}

/**
 * @description This function handles the response from the update check service.
 *
 * The helper does three things:
 *
 * 1. Updates the time in the settings table to know when we can execute the next update check request.
 * 2. Iterates over the received notifications and filters them out based on your notification groups.
 * 3. Calls a custom helper to generate a Ghost notification for the database.
 *
 * The structure of the response is:
 *
 * {
 *  id: 20,
 *  version: 'all4',
 *  messages:
 *     [{
 *          id: 'f8ff6c80-aa61-11e7-a126-6119te37e2b8',
 *          version: '^2',
 *          content: 'Hallouuuu custom',
 *          top: true,
 *          dismissible: true,
 *          type: 'info'
 *      }],
 *  created_at: '2021-10-06T07:00:00.000Z',
 *  custom: 1,
 *  next_check: 1555608722
 * }
 *
 *
 * Example for grouped custom notifications in config:
 *
 *  "notificationGroups": ["migration", "something"]
 *
 * The group 'all' is a reserved name for general custom notifications, which every self hosted blog can receive.
 *
 * @param {Object} response
 * @return {Promise}
 */
async function updateCheckResponse(response) {
    let notifications = [];
    let notificationGroups = (config.get('notificationGroups') || []).concat(['all']);

    debug('Notification Groups', notificationGroups);
    debug('Response Update Check Service', response);

    await api.settings.edit({
        settings: [{
            key: 'next_update_check',
            value: response.next_check
        }]
    }, internal);

    /**
     * @NOTE:
     *
     * When we refactored notifications in Ghost 1.20, the service did not support returning multiple messages.
     * But we wanted to already add the support for future functionality.
     * That's why this helper supports two ways: returning an array of messages or returning an object with
     * a "notifications" key. The second one is probably the best, because we need to support "next_check"
     * on the root level of the response.
     */
    if (_.isArray(response)) {
        notifications = response;
    } else if ((Object.prototype.hasOwnProperty.call(response, 'notifications') && _.isArray(response.notifications))) {
        notifications = response.notifications;
    } else {
        // CASE: default right now
        notifications = [response];
    }

    // CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
    if (notificationGroups.length) {
        notifications = notifications.filter(function (notification) {
            // CASE: release notification, keep
            if (!notification.custom) {
                return true;
            }

            // CASE: filter out messages based on your groups
            return _.includes(notificationGroups.map(function (groupIdentifier) {
                if (notification.version.match(new RegExp(groupIdentifier))) {
                    return true;
                }

                return false;
            }), true) === true;
        });
    }

    return Promise.each(notifications, createCustomNotification);
}

/**
 * @description Entry point to trigger the update check unit.
 *
 * Based on a settings value, we check if `next_update_check` is less than now to decide whether
 * we should request the update check service (http://updates.ghost.org) or not.
 *
 * @returns {Promise}
 */
async function updateCheck() {
    // CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
    if (_.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
        return;
    }

    const result = await api.settings.read(_.extend({key: 'next_update_check'}, internal));

    const nextUpdateCheck = result.settings[0];

    // CASE: Next update check should happen now?
    // @NOTE: You can skip this check by adding a config value. This is helpful for developing.
    if (!config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
        return;
    }

    try {
        const response = await updateCheckRequest();

        await updateCheckResponse(response);
    } catch (err) {
        updateCheckError(err);
    }
}

module.exports = updateCheck;
<|endoftext|>


<commit_before>'use strict';

(function() {
  var lemur = angular
    .module('lemur', [
      'ui.router',
      'ngTable',
      'ngAnimate',
      'chart.js',
      'restangular',
      'angular-loading-bar',
      'ui.bootstrap',
      'angular-spinkit',
      'toaster',
      'uiSwitch',
      'mgo-angular-wizard',
      'satellizer',
      'ngLetterAvatar',
      'angular-clipboard',
      'ngFileSaver'
    ]);

  fetchData().then(bootstrapApplication);

  function fetchData() {
    var initInjector = angular.injector(['ng']);
    var $http = initInjector.get('$http');

    return $http.get('http://localhost:8000/api/1/auth/providers').then(function(response) {
      lemur.constant('providers', response.data);
    }, function(errorResponse) {
      // Handle error case
    });
  }

  function bootstrapApplication() {
    angular.element(document).ready(function() {
      angular.bootstrap(document, ["lemur"]);
    });
  }

  lemur.config(function ($stateProvider, $urlRouterProvider, $authProvider, providers) {
    $urlRouterProvider.otherwise('/welcome');
    $stateProvider
      .state('welcome', {
        url: '/welcome',
        templateUrl: 'angular/welcome/welcome.html'
      });

    _.each(providers, function(provider) {
      if ($authProvider.hasOwnProperty(provider.name)) {
        $authProvider[provider.name] = provider;
      } else {
        $authProvider.oauth2(provider);
      }
    });
  });

  lemur.service('MomentService', function () {
    this.diffMoment = function (start, end) {
      if (end !== 'None') {
        return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
      }
      return 'Unknown';
    };
    this.createMoment = function (date) {
      if (date !== 'None') {
        return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
      }
      return 'Unknown';
    };
  });

  lemur.controller('datePickerController', function ($scope, $timeout){
    $scope.open = function() {
      $timeout(function() {
        $scope.opened = true;
      });
    };
  });

  lemur.service('DefaultService', function (LemurRestangular) {
    var DefaultService = this;
    DefaultService.get = function () {
      return LemurRestangular.all('defaults').customGET().then(function (defaults) {
        return defaults;
      });
    };
  });

  lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
    return Restangular.withConfig(function (RestangularConfigurer) {
      RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
      RestangularConfigurer.setDefaultHttpFields({withCredentials: true});

      RestangularConfigurer.addResponseInterceptor(function (data, operation) {
        var extractedData;

        // .. to look for getList operations
        if (operation === 'getList') {
          // .. and handle the data and meta data
          extractedData = data.items;
          extractedData.total = data.total;
        } else {
          extractedData = data;
        }

        return extractedData;
      });

      RestangularConfigurer.setErrorInterceptor(function(response) {
        if (response.status === 400) {
          if (response.data.message) {
            var data = '';
            _.each(response.data.message, function (value, key) {
              data = data + ' ' + key + ' ' + value;
            });
            response.data.message = data;
          }
        }
      });

      RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params) {
        // We want to make sure the user is auth'd before any requests
        if (!$auth.isAuthenticated()) {
          $location.path('/login');
          return false;
        }

        var regExp = /\[([^)]+)\]/;

        var s = 'sorting';
        var f = 'filter';
        var newParams = {};
        for (var item in params) {
          if (item.indexOf(s) > -1) {
            newParams.sortBy = regExp.exec(item)[1];
            newParams.sortDir = params[item];
          } else if (item.indexOf(f) > -1) {
            var key = regExp.exec(item)[1];
            newParams.filter = key + ';' + params[item];
          } else {
            newParams[item] = params[item];
          }
        }
        return { params: newParams };
      });

    });
  });

  lemur.run(['$templateCache', function ($templateCache) {
    $templateCache.put('ng-table/pager.html', '

 <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default">  </button>

 «   …  »

');
  }]);
}());

<commit_msg>Fixing linting<commit_after>'use strict';

(function() {
  var lemur = angular
    .module('lemur', [
      'ui.router',
      'ngTable',
      'ngAnimate',
      'chart.js',
      'restangular',
      'angular-loading-bar',
      'ui.bootstrap',
      'angular-spinkit',
      'toaster',
      'uiSwitch',
      'mgo-angular-wizard',
      'satellizer',
      'ngLetterAvatar',
      'angular-clipboard',
      'ngFileSaver'
    ]);

  function fetchData() {
    var initInjector = angular.injector(['ng']);
    var $http = initInjector.get('$http');

    return $http.get('http://localhost:8000/api/1/auth/providers').then(function(response) {
      lemur.constant('providers', response.data);
    }, function(errorResponse) {
      console.log('Could not fetch SSO providers')
    });
  }

  function bootstrapApplication() {
    angular.element(document).ready(function() {
      angular.bootstrap(document, ['lemur']);
    });
  }

  fetchData().then(bootstrapApplication);

  lemur.config(function ($stateProvider, $urlRouterProvider, $authProvider, providers) {
    $urlRouterProvider.otherwise('/welcome');
    $stateProvider
      .state('welcome', {
        url: '/welcome',
        templateUrl: 'angular/welcome/welcome.html'
      });

    _.each(providers, function(provider) {
      if ($authProvider.hasOwnProperty(provider.name)) {
        $authProvider[provider.name] = provider;
      } else {
        $authProvider.oauth2(provider);
      }
    });
  });

  lemur.service('MomentService', function () {
    this.diffMoment = function (start, end) {
      if (end !== 'None') {
        return moment(end, 'YYYY-MM-DD HH:mm Z').diff(moment(start, 'YYYY-MM-DD HH:mm Z'), 'minutes') + ' minutes';
      }
      return 'Unknown';
    };
    this.createMoment = function (date) {
      if (date !== 'None') {
        return moment(date, 'YYYY-MM-DD HH:mm Z').fromNow();
      }
      return 'Unknown';
    };
  });

  lemur.controller('datePickerController', function ($scope, $timeout){
    $scope.open = function() {
      $timeout(function() {
        $scope.opened = true;
      });
    };
  });

  lemur.service('DefaultService', function (LemurRestangular) {
    var DefaultService = this;
    DefaultService.get = function () {
      return LemurRestangular.all('defaults').customGET().then(function (defaults) {
        return defaults;
      });
    };
  });

  lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
    return Restangular.withConfig(function (RestangularConfigurer) {
      RestangularConfigurer.setBaseUrl('http://localhost:8000/api/1');
      RestangularConfigurer.setDefaultHttpFields({withCredentials: true});

      RestangularConfigurer.addResponseInterceptor(function (data, operation) {
        var extractedData;

        // .. to look for getList operations
        if (operation === 'getList') {
          // .. and handle the data and meta data
          extractedData = data.items;
          extractedData.total = data.total;
        } else {
          extractedData = data;
        }

        return extractedData;
      });

      RestangularConfigurer.setErrorInterceptor(function(response) {
        if (response.status === 400) {
          if (response.data.message) {
            var data = '';
            _.each(response.data.message, function (value, key) {
              data = data + ' ' + key + ' ' + value;
            });
            response.data.message = data;
          }
        }
      });

      RestangularConfigurer.addFullRequestInterceptor(function (element, operation, route, url, headers, params) {
        // We want to make sure the user is auth'd before any requests
        if (!$auth.isAuthenticated()) {
          $location.path('/login');
          return false;
        }

        var regExp = /\[([^)]+)\]/;

        var s = 'sorting';
        var f = 'filter';
        var newParams = {};
        for (var item in params) {
          if (item.indexOf(s) > -1) {
            newParams.sortBy = regExp.exec(item)[1];
            newParams.sortDir = params[item];
          } else if (item.indexOf(f) > -1) {
            var key = regExp.exec(item)[1];
            newParams.filter = key + ';' + params[item];
          } else {
            newParams[item] = params[item];
          }
        }
        return { params: newParams };
      });

    });
  });

  lemur.run(['$templateCache', function ($templateCache) {
    $templateCache.put('ng-table/pager.html', '

 <button ng-repeat="count in params.settings().counts" type="button" ng-class="{\'active\':params.count()==count}" ng-click="params.count(count)" class="btn btn-default">  </button>

 «   …  »

');
  }]);
}());

<|endoftext|>