<commit_before>/* global ga:true */

import _ from 'underscore';
import m from 'mithril';
import classNames from '../classnames.js';
import Game from '../models/game.js';
import GridComponent from './grid.js';
import DashboardComponent from './dashboard.js';
import ScoreboardComponent from './scoreboard.js';

// The game UI, encompassing all UI pertaining to the game directly
class GameComponent {

  oninit({ attrs: { session, roomCode } }) {
    this.session = session;
    this.game = new Game({
      session,
      // Only enable debug mode on non-production sites
      debug: (window.location.host !== 'projects.calebevans.me' && !window.__karma__)
    });
    if (roomCode) {
      this.session.connect();
      this.session.on('connect', () => {
        this.joinRoom(roomCode);
        this.listenForNewPlayers();
        this.listenForEndGame();
      });
    }
  }

  joinRoom(roomCode) {
    let playerId = this.session.localPlayerId;
    // Join the room immediately if a room code is specified in the URL
    this.session.emit('join-room', { roomCode, playerId }, ({ game, localPlayer }) => {
      console.log('join room', roomCode);
      if (this.session.status === 'returningPlayer') {
        console.log('resume existing game', localPlayer);
      } else if (this.session.status === 'newPlayer') {
        console.log('new player');
      }
      if (game && localPlayer) {
        this.game.restoreFromServer({ game, localPlayer });
      }
      m.redraw();
    });
  }

  listenForNewPlayers() {
    // If P1 leaves and rejoins the game before P2 joins, make sure to
    // listen for P2 joining
    this.session.on('add-player', ({ game, localPlayer }) => {
      this.game.restoreFromServer({ game, localPlayer });
      m.redraw();
    });
  }

  listenForEndGame() {
    this.session.on('end-game', ({ requestingPlayer }) => {
      this.game.requestingPlayer = requestingPlayer;
      this.game.endGame();
      m.redraw();
    });
  }

  // Send the specified game data to the analytics server
  sendAnalytics(args) {
    if (typeof ga === 'function' && !this.game.debug) {
      ga('send', 'event', _.extend({
        eventCategory: this.game.type === '1P' ? '1-Player Game' : '2-Player Game'
      }, args));
    }
  }

  view({ attrs: { roomCode } }) {
    return m('div#game', {
      class: classNames({ 'in-progress': this.game.inProgress })
    }, [
      m('div.game-column', [
        m('h1', 'Connect Four'),
        m(DashboardComponent, {
          game: this.game,
          session: this.session,
          roomCode
        })
      ]),
      m('div.game-column', [
        m(GridComponent, { game: this.game, session: this.session }),
        m(ScoreboardComponent, { game: this.game, session: this.session })
      ])
    ]);
  }

}

export default GameComponent;
<commit_msg>Remove leftover GA code<commit_after>import m from 'mithril';
import classNames from '../classnames.js';
import Game from '../models/game.js';
import GridComponent from './grid.js';
import DashboardComponent from './dashboard.js';
import ScoreboardComponent from './scoreboard.js';

// The game UI, encompassing all UI pertaining to the game directly
class GameComponent {

  oninit({ attrs: { session, roomCode } }) {
    this.session = session;
    this.game = new Game({
      session,
      // Only enable debug mode on non-production sites
      debug: (window.location.host !== 'projects.calebevans.me' && !window.__karma__)
    });
    if (roomCode) {
      this.session.connect();
      this.session.on('connect', () => {
        this.joinRoom(roomCode);
        this.listenForNewPlayers();
        this.listenForEndGame();
      });
    }
  }

  joinRoom(roomCode) {
    let playerId = this.session.localPlayerId;
    // Join the room immediately if a room code is specified in the URL
    this.session.emit('join-room', { roomCode, playerId }, ({ game, localPlayer }) => {
      console.log('join room', roomCode);
      if (this.session.status === 'returningPlayer') {
        console.log('resume existing game', localPlayer);
      } else if (this.session.status === 'newPlayer') {
        console.log('new player');
      }
      if (game && localPlayer) {
        this.game.restoreFromServer({ game, localPlayer });
      }
      m.redraw();
    });
  }

  listenForNewPlayers() {
    // If P1 leaves and rejoins the game before P2 joins, make sure to
    // listen for P2 joining
    this.session.on('add-player', ({ game, localPlayer }) => {
      this.game.restoreFromServer({ game, localPlayer });
      m.redraw();
    });
  }

  listenForEndGame() {
    this.session.on('end-game', ({ requestingPlayer }) => {
      this.game.requestingPlayer = requestingPlayer;
      this.game.endGame();
      m.redraw();
    });
  }

  view({ attrs: { roomCode } }) {
    return m('div#game', {
      class: classNames({ 'in-progress': this.game.inProgress })
    }, [
      m('div.game-column', [
        m('h1', 'Connect Four'),
        m(DashboardComponent, {
          game: this.game,
          session: this.session,
          roomCode
        })
      ]),
      m('div.game-column', [
        m(GridComponent, { game: this.game, session: this.session }),
        m(ScoreboardComponent, { game: this.game, session: this.session })
      ])
    ]);
  }

}

export default GameComponent;
<|endoftext|>


<commit_before>function reveal(evt, modal) {
	document.getElementById(modal).style.display = "block";
}

function hideReveal(evt, modal) {
	document.getElementById(modal).style.display = 'none';
}

function sleep (time) {
  return new Promise(function(resolve) {
  						setTimeout(resolve(), time);
  						}
  					);
}

function createRecording(url){
	var result = confirm("Are you sure you want to create this rule?");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		sleep(500).then(function() {
			openSearchPage();
		})
	}
}

function confirmDeleteRecording(url){
	var result = confirm("Are you sure you want to delete this recording?\nThis action cannot be undone!");
	if (result){
		var request = new XMLHttpRequest();
		request.onreadystatechange = function() {
			if (request.readyState == 4 && request.status == 200) {
			   alert("Recording deleted successfully.");
				openRecordingsPage();
			}
			if (request.readyState == 4 && request.status == 400) {
			   alert("Deletion failed.  Please review the HDHomeRun Record Engine log.");
			}
		};
		request.open("GET", url, true);
		request.send(null);
	}
}

function confirmDeleteRule(url){
	var result = confirm("Are you sure you want to delete this rule? ");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		openSearchPage();
	}
}

function confirmDeleteRule2(url){
	var result = confirm("Are you sure you want to delete this rule? ");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		openRulesPage();
	}
}

function openTab(evt, tabname) {
	var i, tabcontent, tablinks;
	// get elements with class="tabcontent" and hide
	tabcontent = document.getElementsByClassName("tabcontent");
	for (i=-0; i < tabcontent.length; i++) {
		tabcontent[i].style.display = "none";
	}

	// get elements with class="tablink" and remove the active
	tablinks = document.getElementsByClassName("tablink");
	for (i=0; i < tablinks.length; i++) {
		tablinks[i].className = tablinks[i].className.replace(" active", "");
	}

	// load the page
	if (tabname == 'recordings_page') {
		openRecordingsPage();
	}
	if (tabname == 'rules_page') {
		openRulesPage();
	}
	if (tabname == 'search_page') {
		openSearchPage();
	}
	if (tabname == 'hdhr_page') {
		openHDHRPage();
	}
	if (tabname == 'upcoming_page') {
		openUpcomingPage();
	}

	//show the tablinks
	document.getElementById(tabname).style.display = "block";
	evt.currentTarget.className += " active";
}

/* Set the status message */
function setStatus(msg)
{
	isStatusIdle = 0;
	if(msg == '' || msg == null || msg == undefined)
	{
		isStatusIdle = 1;
		msg = "Idle.";
	}
	document.getElementById('statusMessage').innerHTML = msg;
}

function goSearch()
{
	var str = document.getElementById('searchString').value;
	openSearchPage(str);
}
<commit_msg>Update hdhomerundvr_ui.js<commit_after>function reveal(evt, modal) {
	document.getElementById(modal).style.display = "block";
}

function hideReveal(evt, modal) {
	document.getElementById(modal).style.display = 'none';
}

function sleep (time) {
  return new Promise(function(resolve) {
  						setTimeout(resolve(), time);
  						}
  					);
}

function createRecording(url){
	var result = confirm("Are you sure you want to create this rule?");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		sleep(500).then(function() {
			openSearchPage();
		})
	}
}

function confirmDeleteRecording(url){
	var result = confirm("Are you sure you want to delete this recording?\nThis action cannot be undone!");
	if (result){
		var request = new XMLHttpRequest();
		request.onreadystatechange = function() {
			if (request.readyState == 4 && request.status == 200) {
			   alert("Recording deleted successfully.");
				openRecordingsPage();
			}
			if (request.readyState == 4 && request.status == 400) {
			   alert("Deletion failed.  The url was " + url);
			}
		};
		request.open("GET", url, true);
		request.send(null);
	}
}

function confirmDeleteRule(url){
	var result = confirm("Are you sure you want to delete this rule? ");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		openSearchPage();
	}
}

function confirmDeleteRule2(url){
	var result = confirm("Are you sure you want to delete this rule? ");
	if (result){
		var request = new XMLHttpRequest();
		request.open("GET", url, true);
		request.send(null);
		openRulesPage();
	}
}

function openTab(evt, tabname) {
	var i, tabcontent, tablinks;
	// get elements with class="tabcontent" and hide
	tabcontent = document.getElementsByClassName("tabcontent");
	for (i=-0; i < tabcontent.length; i++) {
		tabcontent[i].style.display = "none";
	}

	// get elements with class="tablink" and remove the active
	tablinks = document.getElementsByClassName("tablink");
	for (i=0; i < tablinks.length; i++) {
		tablinks[i].className = tablinks[i].className.replace(" active", "");
	}

	// load the page
	if (tabname == 'recordings_page') {
		openRecordingsPage();
	}
	if (tabname == 'rules_page') {
		openRulesPage();
	}
	if (tabname == 'search_page') {
		openSearchPage();
	}
	if (tabname == 'hdhr_page') {
		openHDHRPage();
	}
	if (tabname == 'upcoming_page') {
		openUpcomingPage();
	}

	//show the tablinks
	document.getElementById(tabname).style.display = "block";
	evt.currentTarget.className += " active";
}

/* Set the status message */
function setStatus(msg)
{
	isStatusIdle = 0;
	if(msg == '' || msg == null || msg == undefined)
	{
		isStatusIdle = 1;
		msg = "Idle.";
	}
	document.getElementById('statusMessage').innerHTML = msg;
}

function goSearch()
{
	var str = document.getElementById('searchString').value;
	openSearchPage(str);
}
<|endoftext|>


<commit_before>'use strict';

const fs = require('fs');
const path = require('path');
const exec = require('child_process').exec;
const async = require('async');
const Slack = require('slack-node');
const Cmr1Cli = require('cmr1-cli');

const MAX_NOTIFICATIONS = 10;

const requiredOptions = [
  'directory',
  'certfile',
  'keyfile',
  'time'
];

class SslValidator extends Cmr1Cli {
  constructor(options) {
    super(options);

    this.slack = new Slack();
    this.failures = [];
    this.groupList = {};
    this.fileTypes = {
      x509: new RegExp(this.options.certfile),
      rsa: new RegExp(this.options.keyfile)
    };

    if (this.options.slack) {
      this.slack.setWebhook(this.options.slack);
    }
  }

  run() {
    this.ensureOptions(err => {
      if (err) this.fail(err);

      const dirs = Array.isArray(this.options.directory) ? this.options.directory : [ this.options.directory ];

      async.each(dirs, (dir, next) => {
        this.findStats(dir, (err, stats) => {
          if (err) return next(err);

          if (stats.isDirectory()) {
            this.processGroup(stats.realPath, next);
          } else {
            this.warn(`${dir} is not a directory!`);
            return next();
          }
        });
      }, err => {
        if (err) {
          this.fail(err);
        } else if (this.failures.length > 0) {
          this.failures.forEach(failure => {
            this.error(`Invalid: ${failure || 'Unknown'}`);
          });
          this.fail(`Failed with ${this.failures.length} error(s)`);
        } else {
          this.finish('Finished.');
        }
      });
    });
  }

  processGroup(path, callback) {
    this.scan(path, (err, group) => {
      if (err) return callback(err);

      this.validateGroup(group, err => {
        if (err) {
          this.warn(err);
          this.failures.push(`${group.dir}::${err}`);
        }

        return callback();
      });
    });
  }

  scan(dir, callback) {
    this.debug(`Scanning dir: ${dir}`);

    fs.readdir(dir, (err, files) => {
      if (err) return callback(err);

      const group = {
        dir,
        mod: null,
        files: []
      };

      async.each(files, (file, next) => {
        this.findStats(path.join(dir, file), (err, stats) => {
          if (err) return next(err);

          if (stats.isDirectory()) {
            if (this.options.recursive) {
              this.processGroup(stats.realPath, next);
            } else {
              this.debug(`Ignoring directory: '${file}'. Set --recursive option to scan recursively.`);

              return next();
            }
          } else if (this.fileTypes.x509.test(file) || this.fileTypes.rsa.test(file)) {
            group.files.push(path.join(dir, file));

            return next();
          } else {
            this.debug(`Skipping file: ${file}`);
            return next();
          }
        });
      }, err => {
        if (err) return callback(err);

        return callback(null, group);
      });
    });
  }

  validateGroup(group, callback) {
    if (group.files && Array.isArray(group.files)) {
      this.debug('Validating group:'+group.dir);
      async.each(group.files, (file, next) => {
        const cmd = this.fileTypes.rsa.test(path.basename(file)) ? 'rsa' : 'x509';

        const flags = [
          '-noout',
          '-modulus'
        ];

        if (cmd === 'x509') {
          flags.push('-dates');
        }

        exec(`openssl ${cmd} ${flags.join(' ')} -in ${file}`, (error, stdout, stderr) => {
          if (error) return next(error);

          if (stderr) {
            this.warn(stderr);
          }

          this.debug(stdout);

          const modMatches = stdout.match(/Modulus\=([^\s]+)/i);
          const dateMatches = stdout.match(/(not(Before|After)\=.*)/gi);

          if (dateMatches && dateMatches.length > 1) {
            // 30day * 24hr * 60min * 60sec * 1000ms
            const expireDiff = this.options.time * 24 * 60 * 60 * 1000;
            const notBeforeStr = dateMatches[0].split('=')[1];
            const notAfterStr = dateMatches[1].split('=')[1];
            const notBefore = new Date(notBeforeStr).getTime();
            const notAfter = new Date(notAfterStr).getTime();
            const now = new Date().getTime();

            if (notBefore > now) {
              return next(`Certificate file: ${file} is not valid before: ${notBeforeStr}`);
            } else if (now >= notAfter) {
              return next(`Certificate file: ${file} is not valid after: ${notAfterStr}`);
            } else if (now >= (notAfter - expireDiff)) {
              return next(`Certificate file: ${file} is expiring in < ${this.options.time} days!`);
            }
          } else if (cmd === 'x509') {
            return next(`Unable to obtain dates from file: ${file}`);
          }

          if (modMatches && modMatches.length > 1) {
            if (!group.mod) {
              group.mod = modMatches[1];
              return next();
            } else if (group.mod !== modMatches[1]) {
              this.warn(`Group MOD = "${group.mod}"`);
              this.warn(` File MOD = "${modMatches[1]}"`);
              return next(`Modulus mismatch!`)
            } else {
              this.debug(`Validated file: ${file}`);
              return next();
            }
          } else {
            return next(`Unable to obtain modulus from file: ${file}`);
          }
        });
      }, err => {
        if (err) return callback(err);

        if (group.files.length > 0) {
          this.success(`Validated: ${group.dir}`);
        }

        return callback();
      });
    } else {
      return callback(`Group: '${group}' is missing files`);
    }
  }

  findStats(path, callback) {
    fs.realpath(path, (err, realPath) => {
      if (err) return callback(err);

      if (realPath !== path) {
          this.debug(`Path: '${path}' resolves to: '${realPath}'`);
      }

      fs.lstat(realPath, (err, stats) => {
        if (err) return callback(err);

        stats.realPath = realPath;

        return callback(null, stats);
      });
    });
  }

  hook(code, callback) {
    if (this.options.hook) {
      this.debug(`Executing hook: ${this.options.hook}`);

      this.findStats(this.options.hook, (err, stats) => {
        if (err) return callback(err);

        exec(`${this.options.hook} ${code} "${this.failures.join(';;')}"`, (error, stdout, stderr) => {
          if (error) return callback(error);

          this.log(stdout);

          if (stderr) {
            this.warn(stderr);
          }

          return callback();
        });
      });
    } else {
      return callback();
    }
  }

  notify(callback) {
    if (this.failures.length <= MAX_NOTIFICATIONS) {
      async.each(this.failures, (failure, next) => {
        const parts = failure.split('::');
        const group = parts[0] || 'Unknown';
        const msg = parts[1] || 'Unknown';

        this.slack.webhook({
          icon_emoji: ':lock:',
          username: 'ssl-validator',
          attachments: [
            {
              fallback: 'SSL Validation Failure!',
              pretext: 'SSL Validation Failure!',
              color: '#D00000',
              fields: [
                {
                  title: group,
                  value: msg,
                  short: false
                }
              ]
            }
          ]
        }, (err, resp) => {
          if (err) return next(err);

          this.debug('Slack webhook response:', resp);

          return next();
        });
      }, callback);
    } else {
      this.slack.webhook({
        icon_emoji: ':lock:',
        username: 'ssl-validator',
        attachments: [
          {
            fallback: 'SSL Validation Failure!',
            pretext: 'SSL Validation Failure!',
            color: '#D00000',
            fields: [
              {
                title: `More than ${MAX_NOTIFICATIONS} failure(s)!`,
                value: `${this.failures.length} total SSL validation failure(s)`,
                short: false
              }
            ]
          }
        ]
      }, (err, resp) => {
        if (err) return callback(err);

        this.debug('Slack webhook response:', resp);

        return callback();
      })
    }
  }

  fail(msg, code=1) {
    this.error(msg);

    this.hook(code, err => {
      if (err) this.error(err);

      if (this.options.slack && this.slack) {
        this.notify(err => {
          if (err) this.error(err);

          process.exit(code);
        });
      } else {
        process.exit(code);
      }
    });
  }

  finish(msg, code=0) {
    this.success(msg);

    this.hook(code, err => {
      if (err) {
        this.fail(err);
      } else {
        process.exit(code);
      }
    });
  }

  ensureOptions(callback) {
    requiredOptions.forEach(option => {
      if (typeof this.options[option] === 'undefined') {
        return callback(`Missing required option: '${option}'`);
      }
    });

    return callback();
  }
}

module.exports = SslValidator;<commit_msg>Simplify error messaging<commit_after>'use strict';

const fs = require('fs');
const path = require('path');
const exec = require('child_process').exec;
const async = require('async');
const Slack = require('slack-node');
const Cmr1Cli = require('cmr1-cli');

const MAX_NOTIFICATIONS = 10;

const requiredOptions = [
  'directory',
  'certfile',
  'keyfile',
  'time'
];

class SslValidator extends Cmr1Cli {
  constructor(options) {
    super(options);

    this.slack = new Slack();
    this.failures = [];
    this.groupList = {};
    this.fileTypes = {
      x509: new RegExp(this.options.certfile),
      rsa: new RegExp(this.options.keyfile)
    };

    if (this.options.slack) {
      this.slack.setWebhook(this.options.slack);
    }
  }

  run() {
    this.ensureOptions(err => {
      if (err) this.fail(err);

      const dirs = Array.isArray(this.options.directory) ? this.options.directory : [ this.options.directory ];

      async.each(dirs, (dir, next) => {
        this.findStats(dir, (err, stats) => {
          if (err) return next(err);

          if (stats.isDirectory()) {
            this.processGroup(stats.realPath, next);
          } else {
            this.warn(`${dir} is not a directory!`);
            return next();
          }
        });
      }, err => {
        if (err) {
          this.fail(err);
        } else if (this.failures.length > 0) {
          this.failures.forEach(failure => {
            this.error(failure);
          });
          this.fail(`Failed with ${this.failures.length} error(s)`);
        } else {
          this.finish('Finished.');
        }
      });
    });
  }

  processGroup(path, callback) {
    this.scan(path, (err, group) => {
      if (err) return callback(err);

      this.validateGroup(group, err => {
        if (err) {
          this.warn(err);
          this.failures.push(`${group.dir}::${err}`);
        }

        return callback();
      });
    });
  }

  scan(dir, callback) {
    this.debug(`Scanning dir: ${dir}`);

    fs.readdir(dir, (err, files) => {
      if (err) return callback(err);

      const group = {
        dir,
        mod: null,
        files: []
      };

      async.each(files, (file, next) => {
        this.findStats(path.join(dir, file), (err, stats) => {
          if (err) return next(err);

          if (stats.isDirectory()) {
            if (this.options.recursive) {
              this.processGroup(stats.realPath, next);
            } else {
              this.debug(`Ignoring directory: '${file}'. Set --recursive option to scan recursively.`);

              return next();
            }
          } else if (this.fileTypes.x509.test(file) || this.fileTypes.rsa.test(file)) {
            group.files.push(path.join(dir, file));

            return next();
          } else {
            this.debug(`Skipping file: ${file}`);
            return next();
          }
        });
      }, err => {
        if (err) return callback(err);

        return callback(null, group);
      });
    });
  }

  validateGroup(group, callback) {
    if (group.files && Array.isArray(group.files)) {
      this.debug('Validating group:'+group.dir);
      async.each(group.files, (file, next) => {
        const cmd = this.fileTypes.rsa.test(path.basename(file)) ? 'rsa' : 'x509';

        const flags = [
          '-noout',
          '-modulus'
        ];

        if (cmd === 'x509') {
          flags.push('-dates');
        }

        exec(`openssl ${cmd} ${flags.join(' ')} -in ${file}`, (error, stdout, stderr) => {
          if (error) return next(error);

          if (stderr) {
            this.warn(stderr);
          }

          this.debug(stdout);

          const modMatches = stdout.match(/Modulus\=([^\s]+)/i);
          const dateMatches = stdout.match(/(not(Before|After)\=.*)/gi);

          if (dateMatches && dateMatches.length > 1) {
            // 30day * 24hr * 60min * 60sec * 1000ms
            const expireDiff = this.options.time * 24 * 60 * 60 * 1000;
            const notBeforeStr = dateMatches[0].split('=')[1];
            const notAfterStr = dateMatches[1].split('=')[1];
            const notBefore = new Date(notBeforeStr).getTime();
            const notAfter = new Date(notAfterStr).getTime();
            const now = new Date().getTime();

            if (notBefore > now) {
              return next(`Certificate file: ${file} is not valid before: ${notBeforeStr}`);
            } else if (now >= notAfter) {
              return next(`Certificate file: ${file} is not valid after: ${notAfterStr}`);
            } else if (now >= (notAfter - expireDiff)) {
              return next(`Certificate file: ${file} is expiring in < ${this.options.time} days!`);
            }
          } else if (cmd === 'x509') {
            return next(`Unable to obtain dates from file: ${file}`);
          }

          if (modMatches && modMatches.length > 1) {
            if (!group.mod) {
              group.mod = modMatches[1];
              return next();
            } else if (group.mod !== modMatches[1]) {
              this.warn(`Group MOD = "${group.mod}"`);
              this.warn(` File MOD = "${modMatches[1]}"`);
              return next(`Modulus mismatch!`)
            } else {
              this.debug(`Validated file: ${file}`);
              return next();
            }
          } else {
            return next(`Unable to obtain modulus from file: ${file}`);
          }
        });
      }, err => {
        if (err) return callback(err);

        if (group.files.length > 0) {
          this.success(`Validated: ${group.dir}`);
        }

        return callback();
      });
    } else {
      return callback(`Group: '${group}' is missing files`);
    }
  }

  findStats(path, callback) {
    fs.realpath(path, (err, realPath) => {
      if (err) return callback(err);

      if (realPath !== path) {
          this.debug(`Path: '${path}' resolves to: '${realPath}'`);
      }

      fs.lstat(realPath, (err, stats) => {
        if (err) return callback(err);

        stats.realPath = realPath;

        return callback(null, stats);
      });
    });
  }

  hook(code, callback) {
    if (this.options.hook) {
      this.debug(`Executing hook: ${this.options.hook}`);

      this.findStats(this.options.hook, (err, stats) => {
        if (err) return callback(err);

        exec(`${this.options.hook} ${code} "${this.failures.join(';;')}"`, (error, stdout, stderr) => {
          if (error) return callback(error);

          this.log(stdout);

          if (stderr) {
            this.warn(stderr);
          }

          return callback();
        });
      });
    } else {
      return callback();
    }
  }

  notify(callback) {
    if (this.failures.length <= MAX_NOTIFICATIONS) {
      async.each(this.failures, (failure, next) => {
        const parts = failure.split('::');
        const group = parts[0] || 'Unknown';
        const msg = parts[1] || 'Unknown';

        this.slack.webhook({
          icon_emoji: ':lock:',
          username: 'ssl-validator',
          attachments: [
            {
              fallback: 'SSL Validation Failure!',
              pretext: 'SSL Validation Failure!',
              color: '#D00000',
              fields: [
                {
                  title: group,
                  value: msg,
                  short: false
                }
              ]
            }
          ]
        }, (err, resp) => {
          if (err) return next(err);

          this.debug('Slack webhook response:', resp);

          return next();
        });
      }, callback);
    } else {
      this.slack.webhook({
        icon_emoji: ':lock:',
        username: 'ssl-validator',
        attachments: [
          {
            fallback: 'SSL Validation Failure!',
            pretext: 'SSL Validation Failure!',
            color: '#D00000',
            fields: [
              {
                title: `More than ${MAX_NOTIFICATIONS} failure(s)!`,
                value: `${this.failures.length} total SSL validation failure(s)`,
                short: false
              }
            ]
          }
        ]
      }, (err, resp) => {
        if (err) return callback(err);

        this.debug('Slack webhook response:', resp);

        return callback();
      })
    }
  }

  fail(msg, code=1) {
    this.error(msg);

    this.hook(code, err => {
      if (err) this.error(err);

      if (this.options.slack && this.slack) {
        this.notify(err => {
          if (err) this.error(err);

          process.exit(code);
        });
      } else {
        process.exit(code);
      }
    });
  }

  finish(msg, code=0) {
    this.success(msg);

    this.hook(code, err => {
      if (err) {
        this.fail(err);
      } else {
        process.exit(code);
      }
    });
  }

  ensureOptions(callback) {
    requiredOptions.forEach(option => {
      if (typeof this.options[option] === 'undefined') {
        return callback(`Missing required option: '${option}'`);
      }
    });

    return callback();
  }
}

module.exports = SslValidator;<|endoftext|>


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

self.IOWA = self.IOWA || {};

IOWA.Schedule = (function() {

  "use strict";

  var SCHEDULE_ENDPOINT = 'api/v1/schedule';
  var SCHEDULE_ENDPOINT_USERS = 'api/v1/user/schedule';
  var SURVEY_ENDPOINT_USERS = 'api/v1/user/survey';
  var QUEUED_SESSION_UPDATES_DB_NAME = 'shed-offline-session-updates';

  var scheduleData_ = null;
  var cache = {
    'userSavedSessions': [],
    'userSavedSurveys': []
  };

  // A promise fulfilled by the loaded schedule.
  var scheduleDeferredPromise = null;

  // The resolve function for scheduleDeferredPromise;
  var scheduleDeferredPromiseResolver = null;

  /**
   * Create the deferred schedule-fetching promise `scheduleDeferredPromise`.
   * @private
   */
  function createScheduleDeferred_() {
    var scheduleDeferred = IOWA.Util.createDeferred();
    scheduleDeferredPromiseResolver = scheduleDeferred.resolve;
    scheduleDeferredPromise = scheduleDeferred.promise.then(function(data) {
      scheduleData_ = data.scheduleData;
      IOWA.Elements.Template.scheduleData = data.scheduleData;
      IOWA.Elements.Template.filterSessionTypes = data.tags.filterSessionTypes;
      IOWA.Elements.Template.filterThemes = data.tags.filterThemes;
      IOWA.Elements.Template.filterTopics = data.tags.filterTopics;

      return scheduleData_;
    });
  }

  /**
   * Fetches the I/O schedule data. If the schedule has not been loaded yet, a
   * network request is kicked off. To wait on the schedule without causing a
   * triggering a request for it, use `schedulePromise`.
   * @return {Promise} Resolves with response schedule data.
   */
  function fetchSchedule() {
    if (scheduleData_) {
      return Promise.resolve(scheduleData_);
    }

    return IOWA.Request.xhrPromise('GET', SCHEDULE_ENDPOINT, false).then(function(resp) {
      scheduleData_ = resp;
      return scheduleData_;
    });
  }

  /**
   * Returns a promise fulfilled when the master schedule is loaded.
   * @return {!Promise} Resolves with response schedule data.
   */
  function schedulePromise() {
    if (!scheduleDeferredPromise) {
      createScheduleDeferred_();
    }

    return scheduleDeferredPromise;
  }

  /**
   * Resolves the schedule-fetching promise.
   * @param {{scheduleData, tags}} data
   */
  function resolveSchedulePromise(data) {
    if (!scheduleDeferredPromiseResolver) {
      createScheduleDeferred_();
    }

    scheduleDeferredPromiseResolver(data);
  }

  /**
   * Fetches the resource from cached value storage or network.
   * If this is the first time it's been called, then uses the cache-then-network strategy to
   * first try to read the data stored in the Cache Storage API, and invokes the callback with that
   * response. It then tries to fetch a fresh copy of the data from the network, saves the response
   * locally in memory, and resolves the promise with that response.
   * @param {string} url The address of the resource.
   * @param {Array} resourceCache A variable name to store the cached resource.
   * @param {function} callback The callback to execute when the user survey data is available.
   */
  function fetchResource(url, resourceCache, callback) {
    if (cache[resourceCache].length) {
      callback(cache[resourceCache]);
    } else {
      var callbackWrapper = function(resource) {
        cache[resourceCache] = resource || [];
        callback(cache[resourceCache]);
      };

      IOWA.Request.cacheThenNetwork(url, callback, callbackWrapper, true);
    }
  }

  /**
   * Fetches the user's saved sessions.
   * If this is the first time it's been called, then uses the cache-then-network strategy to
   * first try to read the data stored in the Cache Storage API, and invokes the callback with that
   * response. It then tries to fetch a fresh copy of the data from the network, saves the response
   * locally in memory, and resolves the promise with that response.
   * @param {function} callback The callback to execute when the user schedule data is available.
   */
  function fetchUserSchedule(callback) {
    fetchResource(SCHEDULE_ENDPOINT_USERS, 'userSavedSessions', callback);
  }

  /**
   * Wait for the master schedule to have loaded, then use `fetchUserSchedule`
   * to fetch the user's schedule and finally bind it for display.
   * loadUserSchedule() doesn't wait for the user to be signed in, so ensure that there is a
   * signed-in user before calling this function.
   */
  function loadUserSchedule() {
    // Only fetch their schedule if the worker has responded with the master schedule.
    schedulePromise().then(function() {
      IOWA.Elements.Template.scheduleFetchingUserData = true;

      // Fetch user's saved sessions.
      fetchUserSchedule(function(savedSessions) {
        var template = IOWA.Elements.Template;
        template.scheduleFetchingUserData = false;
        template.savedSessions = savedSessions;
        updateSavedSessionsUI(template.savedSessions);
      });

      // Fetch user's rated sessions.
      fetchResource(SURVEY_ENDPOINT_USERS, 'userSavedSurveys', function(savedSurveys) {
        var template = IOWA.Elements.Template;
        template.savedSurveys = savedSurveys;
        updateRatedSessions(savedSurveys);
      });
    });
  }

  /**
   * Adds/removes a session from the user's bookmarked sessions.
   * @param {string} sessionId The session to add/remove.
   * @param {Boolean} save True if the session should be added, false if it
   *     should be removed.
   * @return {Promise} Resolves with the server's response.
   */
  function saveSession(sessionId, save) {
    IOWA.Analytics.trackEvent('session', 'bookmark', save ? 'save' : 'remove');

    return IOWA.Auth.waitForSignedIn(
        'Sign in to add events to My Schedule').then(function() {
      IOWA.Elements.Template.scheduleFetchingUserData = true;
      var url = SCHEDULE_ENDPOINT_USERS + '/' + sessionId;
      var method = save ? 'PUT' : 'DELETE';
      return submitSessionRequest(
          url, method, null, 'Unable to modify My Schedule.',
          clearCachedUserSchedule);
    });
  }

  /**
   * Submits session-related request to backend.
   * @param {string} url Request url.
   * @param {string} method Request method, e.g. 'PUT'.
   * @param {Object} payload JSON payload.
   * @param {string} errorMsg Message to be shown on error.
   * @param {function} callback Callback to be called with the resource.
   * @return {Promise} Resolves with the server's response.
   */
  function submitSessionRequest(url, method, payload, errorMsg, callback) {
    return IOWA.Request.xhrPromise(method, url, true, payload)
      .then(callback)
      .catch(function(error) {
        // error will be an XMLHttpRequestProgressEvent if the xhrPromise()
        // was rejected due to a network error.
        // Otherwise, error will be a Error object.
        if ('serviceWorker' in navigator && XMLHttpRequestProgressEvent &&
            error instanceof XMLHttpRequestProgressEvent) {
          IOWA.Elements.Toast.showMessage(
              errorMsg + 'The change will be retried on your next visit.');
        } else {
          IOWA.Elements.Toast.showMessage(errorMsg);
        }
        throw error;
      });

  }

  /**
   * Submits session survey results.
   * @param {string} sessionId The session to be rated.
   * @param {Object} answers An object with question/answer pairs.
   * @return {Promise} Resolves with the server's response.
   */
  function saveSurvey(sessionId, answers) {
    IOWA.Analytics.trackEvent('session', 'rate', sessionId);
    return IOWA.Auth.waitForSignedIn(
        'Sign in to submit feedback').then(function() {
      var url = SURVEY_ENDPOINT_USERS + '/' + sessionId;
      var callback = function(response) {
        IOWA.Elements.Template.savedSurveys = response;
      };
      return submitSessionRequest(
          url, 'PUT', answers, 'Unable to save feedback results.', callback);
    });
  }

  /**
   * Shows a notification when bookmarking/removing a session.
   * @param {Boolean} saved True if the session was saved. False if it was removed.
   * @param {string=} opt_message Optional override message for the
   * "Added to My Schedule" toast.
   */
  function bookmarkSessionNotification(saved, opt_message) {
    var message = opt_message || "You'll get a notification when it starts.";
    var template = IOWA.Elements.Template;

    if (saved) {
      // If IOWA.Elements.Template.dontAutoSubscribe is true, this promise will reject immediately, and we'll just
      // add the session without attempting to auto-subscribe.
      return IOWA.Notifications.subscribePromise(template.dontAutoSubscribe).then(function() {
        IOWA.Elements.Toast.showMessage("Added to My Schedule. " + message);
      }).catch(function(error) {
        template.dontAutoSubscribe = true;
        if (error && error.name === 'AbortError') {
          // AbortError indicates that the subscription couldn't be completed due to the page
          // persmissions for notifications being set to denied.
          IOWA.Elements.Toast.showMessage('Added to My Schedule. Want to enable notifications?', null, 'Learn how', function() {
            window.open('permissions', '_blank');
          });
        } else {
          // If the subscription failed for some other reason, like because we're not
          // auto-subscribing, show the normal toast.
          IOWA.Elements.Toast.showMessage('Added to My Schedule.');
        }
      });
    } else {
      IOWA.Elements.Toast.showMessage('Removed from My Schedule');
    }
  }

  function generateFilters(tags) {
    var filterSessionTypes = [];
    var filterThemes = [];
    var filterTopics = [];

    var sortedTags = Object.keys(tags).map(function(tag) {
      return tags[tag];
    }).sort(function(a, b) {
      if (a.order_in_category < b.order_in_category) {
        return -1;
      }
      if (a.order_in_category > b.order_in_category) {
        return 1;
      }
      return 0;
    });

    for (var i = 0; i < sortedTags.length; ++i) {
      var tag = sortedTags[i];
      switch (tag.category) {
        case 'TYPE':
          filterSessionTypes.push(tag.name);
          break;
        case 'TOPIC':
          filterTopics.push(tag.name);
          break;
        case 'THEME':
          filterThemes.push(tag.name);
          break;
      }
    }

    return {
      filterSessionTypes: filterSessionTypes,
      filterThemes: filterThemes,
      filterTopics: filterTopics
    };
  }

  function updateSavedSessionsUI(savedSessions) {
    //  Mark/unmarked sessions the user has bookmarked.
    var sessions = IOWA.Elements.Template.scheduleData.sessions;
    for (var i = 0; i < sessions.length; ++i) {
      var session = sessions[i];
      session.saved = savedSessions.indexOf(session.id) !== -1;
    }
  }

  function updateRatedSessions(savedSurveys) {
    var sessions = IOWA.Elements.Template.scheduleData.sessions;
    for (var i = 0; i < sessions.length; ++i) {
      var session = sessions[i];
      session.rated = savedSurveys.indexOf(session.id) !== -1;
    }
  }

  function clearCachedUserSchedule() {
    cache['userSavedSessions'] = [];
  }

  /**
   * Clear all user schedule data from display.
   */
  function clearUserSchedule() {
    var template = IOWA.Elements.Template;
    template.savedSessions = [];
    updateSavedSessionsUI(template.savedSessions);
    clearCachedUserSchedule();
  }

  function getSessionById(sessionId) {
    for (var i = 0; i < scheduleData_.sessions.length; ++i) {
      var session = scheduleData_.sessions[i];
      if (session.id === sessionId) {
        return session;
      }
    }
    return null;
  }

  /**
   * Checks to see if there are any failed schedule update requests queued in IndexedDB, and if so,
   * replays them. Should only be called when auth is available, i.e. after login.
   *
   * @return {Promise} Resolves once the replay attempts are done, whether or not they succeeded.
   */
  function replayQueuedRequests() {
    // Only bother checking for queued requests if we're on a browser with service worker support,
    // since they can't be queued otherwise. This has a side effect of working around a bug in
    // Safari triggered by the simpleDB library.
    if ('serviceWorker' in navigator) {
      return simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
        var replayPromises = [];
        // forEach is a special method implemented by SimpleDB, and isn't the normal Array.forEach.
        return db.forEach(function(url, method) {
          var replayPromise = IOWA.Request.xhrPromise(method, url, true).then(function() {
            return db.delete(url).then(function() {
              return true;
            });
          });
          replayPromises.push(replayPromise);
        }).then(function() {
          if (replayPromises.length) {
            return Promise.all(replayPromises).then(function() {
              IOWA.Elements.Toast.showMessage('My Schedule was updated with offline changes.');
            });
          }
        });
      }).catch(function() {
        IOWA.Elements.Toast.showMessage('Offline changes could not be applied to My Schedule.');
      });
    } else {
      return Promise.resolve();
    }
  }

  /**
   * Deletes the IndexedDB database used to queue up failed requests.
   * Useful when, e.g., the user has logged out.
   *
   * @return {Promise} Resolves once the IndexedDB database is deleted.
   */
  function clearQueuedRequests() {
    return simpleDB.delete(QUEUED_SESSION_UPDATES_DB_NAME);
  }

  return {
    bookmarkSessionNotification: bookmarkSessionNotification,
    clearCachedUserSchedule: clearCachedUserSchedule,
    fetchSchedule: fetchSchedule,
    schedulePromise: schedulePromise,
    resolveSchedulePromise: resolveSchedulePromise,
    fetchUserSchedule: fetchUserSchedule,
    loadUserSchedule: loadUserSchedule,
    saveSession: saveSession,
    saveSurvey: saveSurvey,
    generateFilters: generateFilters,
    getSessionById: getSessionById,
    updateSavedSessionsUI: updateSavedSessionsUI,
    replayQueuedRequests: replayQueuedRequests,
    clearQueuedRequests: clearQueuedRequests,
    clearUserSchedule: clearUserSchedule
  };

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

self.IOWA = self.IOWA || {};

IOWA.Schedule = (function() {

  "use strict";

  var SCHEDULE_ENDPOINT = 'api/v1/schedule';
  var SCHEDULE_ENDPOINT_USERS = 'api/v1/user/schedule';
  var SURVEY_ENDPOINT_USERS = 'api/v1/user/survey';
  var QUEUED_SESSION_UPDATES_DB_NAME = 'shed-offline-session-updates';

  var scheduleData_ = null;
  var cache = {
    'userSavedSessions': [],
    'userSavedSurveys': []
  };

  // A promise fulfilled by the loaded schedule.
  var scheduleDeferredPromise = null;

  // The resolve function for scheduleDeferredPromise;
  var scheduleDeferredPromiseResolver = null;

  /**
   * Create the deferred schedule-fetching promise `scheduleDeferredPromise`.
   * @private
   */
  function createScheduleDeferred_() {
    var scheduleDeferred = IOWA.Util.createDeferred();
    scheduleDeferredPromiseResolver = scheduleDeferred.resolve;
    scheduleDeferredPromise = scheduleDeferred.promise.then(function(data) {
      scheduleData_ = data.scheduleData;
      IOWA.Elements.Template.scheduleData = data.scheduleData;
      IOWA.Elements.Template.filterSessionTypes = data.tags.filterSessionTypes;
      IOWA.Elements.Template.filterThemes = data.tags.filterThemes;
      IOWA.Elements.Template.filterTopics = data.tags.filterTopics;

      return scheduleData_;
    });
  }

  /**
   * Fetches the I/O schedule data. If the schedule has not been loaded yet, a
   * network request is kicked off. To wait on the schedule without causing a
   * triggering a request for it, use `schedulePromise`.
   * @return {Promise} Resolves with response schedule data.
   */
  function fetchSchedule() {
    if (scheduleData_) {
      return Promise.resolve(scheduleData_);
    }

    return IOWA.Request.xhrPromise('GET', SCHEDULE_ENDPOINT, false).then(function(resp) {
      scheduleData_ = resp;
      return scheduleData_;
    });
  }

  /**
   * Returns a promise fulfilled when the master schedule is loaded.
   * @return {!Promise} Resolves with response schedule data.
   */
  function schedulePromise() {
    if (!scheduleDeferredPromise) {
      createScheduleDeferred_();
    }

    return scheduleDeferredPromise;
  }

  /**
   * Resolves the schedule-fetching promise.
   * @param {{scheduleData, tags}} data
   */
  function resolveSchedulePromise(data) {
    if (!scheduleDeferredPromiseResolver) {
      createScheduleDeferred_();
    }

    scheduleDeferredPromiseResolver(data);
  }

  /**
   * Fetches the resource from cached value storage or network.
   * If this is the first time it's been called, then uses the cache-then-network strategy to
   * first try to read the data stored in the Cache Storage API, and invokes the callback with that
   * response. It then tries to fetch a fresh copy of the data from the network, saves the response
   * locally in memory, and resolves the promise with that response.
   * @param {string} url The address of the resource.
   * @param {Array} resourceCache A variable name to store the cached resource.
   * @param {function} callback The callback to execute when the user survey data is available.
   */
  function fetchResource(url, resourceCache, callback) {
    if (cache[resourceCache].length) {
      callback(cache[resourceCache]);
    } else {
      var callbackWrapper = function(resource) {
        cache[resourceCache] = resource || [];
        callback(cache[resourceCache]);
      };

      IOWA.Request.cacheThenNetwork(url, callback, callbackWrapper, true);
    }
  }

  /**
   * Fetches the user's saved sessions.
   * If this is the first time it's been called, then uses the cache-then-network strategy to
   * first try to read the data stored in the Cache Storage API, and invokes the callback with that
   * response. It then tries to fetch a fresh copy of the data from the network, saves the response
   * locally in memory, and resolves the promise with that response.
   * @param {function} callback The callback to execute when the user schedule data is available.
   */
  function fetchUserSchedule(callback) {
    fetchResource(SCHEDULE_ENDPOINT_USERS, 'userSavedSessions', callback);
  }

  /**
   * Wait for the master schedule to have loaded, then use `fetchUserSchedule`
   * to fetch the user's schedule and finally bind it for display.
   * loadUserSchedule() doesn't wait for the user to be signed in, so ensure that there is a
   * signed-in user before calling this function.
   */
  function loadUserSchedule() {
    // Only fetch their schedule if the worker has responded with the master schedule.
    schedulePromise().then(function() {
      IOWA.Elements.Template.scheduleFetchingUserData = true;

      // Fetch user's saved sessions.
      fetchUserSchedule(function(savedSessions) {
        var template = IOWA.Elements.Template;
        template.scheduleFetchingUserData = false;
        template.savedSessions = savedSessions;
        updateSavedSessionsUI(template.savedSessions);
      });

      // Fetch user's rated sessions.
      fetchResource(SURVEY_ENDPOINT_USERS, 'userSavedSurveys', function(savedSurveys) {
        var template = IOWA.Elements.Template;
        template.savedSurveys = savedSurveys;
        updateRatedSessions(savedSurveys);
      });
    });
  }

  /**
   * Adds/removes a session from the user's bookmarked sessions.
   * @param {string} sessionId The session to add/remove.
   * @param {Boolean} save True if the session should be added, false if it
   *     should be removed.
   * @return {Promise} Resolves with the server's response.
   */
  function saveSession(sessionId, save) {
    IOWA.Analytics.trackEvent('session', 'bookmark', save ? 'save' : 'remove');

    return IOWA.Auth.waitForSignedIn(
        'Sign in to add events to My Schedule').then(function() {
      IOWA.Elements.Template.scheduleFetchingUserData = true;
      var url = SCHEDULE_ENDPOINT_USERS + '/' + sessionId;
      var method = save ? 'PUT' : 'DELETE';
      return submitSessionRequest(
          url, method, null, 'Unable to modify My Schedule.',
          clearCachedUserSchedule);
    });
  }

  /**
   * Submits session-related request to backend.
   * @param {string} url Request url.
   * @param {string} method Request method, e.g. 'PUT'.
   * @param {Object} payload JSON payload.
   * @param {string} errorMsg Message to be shown on error.
   * @param {function} callback Callback to be called with the resource.
   * @return {Promise} Resolves with the server's response.
   */
  function submitSessionRequest(url, method, payload, errorMsg, callback) {
    return IOWA.Request.xhrPromise(method, url, true, payload)
      .then(callback)
      .catch(function(error) {
        // error will be an XMLHttpRequestProgressEvent if the xhrPromise()
        // was rejected due to a network error.
        // Otherwise, error will be a Error object.
        if ('serviceWorker' in navigator && XMLHttpRequestProgressEvent &&
            error instanceof XMLHttpRequestProgressEvent) {
          IOWA.Elements.Toast.showMessage(
              errorMsg + ' The change will be retried on your next visit.');
        } else {
          IOWA.Elements.Toast.showMessage(errorMsg);
        }
        throw error;
      });

  }

  /**
   * Submits session survey results.
   * @param {string} sessionId The session to be rated.
   * @param {Object} answers An object with question/answer pairs.
   * @return {Promise} Resolves with the server's response.
   */
  function saveSurvey(sessionId, answers) {
    IOWA.Analytics.trackEvent('session', 'rate', sessionId);
    return IOWA.Auth.waitForSignedIn(
        'Sign in to submit feedback').then(function() {
      var url = SURVEY_ENDPOINT_USERS + '/' + sessionId;
      var callback = function(response) {
        IOWA.Elements.Template.savedSurveys = response;
      };
      return submitSessionRequest(
          url, 'PUT', answers, 'Unable to save feedback results.', callback);
    });
  }

  /**
   * Shows a notification when bookmarking/removing a session.
   * @param {Boolean} saved True if the session was saved. False if it was removed.
   * @param {string=} opt_message Optional override message for the
   * "Added to My Schedule" toast.
   */
  function bookmarkSessionNotification(saved, opt_message) {
    var message = opt_message || "You'll get a notification when it starts.";
    var template = IOWA.Elements.Template;

    if (saved) {
      // If IOWA.Elements.Template.dontAutoSubscribe is true, this promise will reject immediately, and we'll just
      // add the session without attempting to auto-subscribe.
      return IOWA.Notifications.subscribePromise(template.dontAutoSubscribe).then(function() {
        IOWA.Elements.Toast.showMessage("Added to My Schedule. " + message);
      }).catch(function(error) {
        template.dontAutoSubscribe = true;
        if (error && error.name === 'AbortError') {
          // AbortError indicates that the subscription couldn't be completed due to the page
          // persmissions for notifications being set to denied.
          IOWA.Elements.Toast.showMessage('Added to My Schedule. Want to enable notifications?', null, 'Learn how', function() {
            window.open('permissions', '_blank');
          });
        } else {
          // If the subscription failed for some other reason, like because we're not
          // auto-subscribing, show the normal toast.
          IOWA.Elements.Toast.showMessage('Added to My Schedule.');
        }
      });
    } else {
      IOWA.Elements.Toast.showMessage('Removed from My Schedule');
    }
  }

  function generateFilters(tags) {
    var filterSessionTypes = [];
    var filterThemes = [];
    var filterTopics = [];

    var sortedTags = Object.keys(tags).map(function(tag) {
      return tags[tag];
    }).sort(function(a, b) {
      if (a.order_in_category < b.order_in_category) {
        return -1;
      }
      if (a.order_in_category > b.order_in_category) {
        return 1;
      }
      return 0;
    });

    for (var i = 0; i < sortedTags.length; ++i) {
      var tag = sortedTags[i];
      switch (tag.category) {
        case 'TYPE':
          filterSessionTypes.push(tag.name);
          break;
        case 'TOPIC':
          filterTopics.push(tag.name);
          break;
        case 'THEME':
          filterThemes.push(tag.name);
          break;
      }
    }

    return {
      filterSessionTypes: filterSessionTypes,
      filterThemes: filterThemes,
      filterTopics: filterTopics
    };
  }

  function updateSavedSessionsUI(savedSessions) {
    //  Mark/unmarked sessions the user has bookmarked.
    var sessions = IOWA.Elements.Template.scheduleData.sessions;
    for (var i = 0; i < sessions.length; ++i) {
      var session = sessions[i];
      session.saved = savedSessions.indexOf(session.id) !== -1;
    }
  }

  function updateRatedSessions(savedSurveys) {
    var sessions = IOWA.Elements.Template.scheduleData.sessions;
    for (var i = 0; i < sessions.length; ++i) {
      var session = sessions[i];
      session.rated = savedSurveys.indexOf(session.id) !== -1;
    }
  }

  function clearCachedUserSchedule() {
    cache.userSavedSessions = [];
  }

  /**
   * Clear all user schedule data from display.
   */
  function clearUserSchedule() {
    var template = IOWA.Elements.Template;
    template.savedSessions = [];
    updateSavedSessionsUI(template.savedSessions);
    clearCachedUserSchedule();
  }

  function getSessionById(sessionId) {
    for (var i = 0; i < scheduleData_.sessions.length; ++i) {
      var session = scheduleData_.sessions[i];
      if (session.id === sessionId) {
        return session;
      }
    }
    return null;
  }

  /**
   * Checks to see if there are any failed schedule update requests queued in IndexedDB, and if so,
   * replays them. Should only be called when auth is available, i.e. after login.
   *
   * @return {Promise} Resolves once the replay attempts are done, whether or not they succeeded.
   */
  function replayQueuedRequests() {
    // Only bother checking for queued requests if we're on a browser with service worker support,
    // since they can't be queued otherwise. This has a side effect of working around a bug in
    // Safari triggered by the simpleDB library.
    if ('serviceWorker' in navigator) {
      return simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
        var replayPromises = [];
        // forEach is a special method implemented by SimpleDB, and isn't the normal Array.forEach.
        return db.forEach(function(url, method) {
          var replayPromise = IOWA.Request.xhrPromise(method, url, true).then(function() {
            return db.delete(url).then(function() {
              return true;
            });
          });
          replayPromises.push(replayPromise);
        }).then(function() {
          if (replayPromises.length) {
            return Promise.all(replayPromises).then(function() {
              IOWA.Elements.Toast.showMessage('My Schedule was updated with offline changes.');
            });
          }
        });
      }).catch(function() {
        IOWA.Elements.Toast.showMessage('Offline changes could not be applied to My Schedule.');
      });
    } else {
      return Promise.resolve();
    }
  }

  /**
   * Deletes the IndexedDB database used to queue up failed requests.
   * Useful when, e.g., the user has logged out.
   *
   * @return {Promise} Resolves once the IndexedDB database is deleted.
   */
  function clearQueuedRequests() {
    return simpleDB.delete(QUEUED_SESSION_UPDATES_DB_NAME);
  }

  return {
    bookmarkSessionNotification: bookmarkSessionNotification,
    clearCachedUserSchedule: clearCachedUserSchedule,
    fetchSchedule: fetchSchedule,
    schedulePromise: schedulePromise,
    resolveSchedulePromise: resolveSchedulePromise,
    fetchUserSchedule: fetchUserSchedule,
    loadUserSchedule: loadUserSchedule,
    saveSession: saveSession,
    saveSurvey: saveSurvey,
    generateFilters: generateFilters,
    getSessionById: getSessionById,
    updateSavedSessionsUI: updateSavedSessionsUI,
    replayQueuedRequests: replayQueuedRequests,
    clearQueuedRequests: clearQueuedRequests,
    clearUserSchedule: clearUserSchedule
  };

})();
<|endoftext|>


<commit_before>'use strict';

console.log('**********************USING MOCK BACKEND**********************');

angular.module('confRegistrationWebApp')
  .run(function ($httpBackend, uuid) {

    $httpBackend.whenGET(/views\/.*/).passThrough();

    var registrations = {
      '012': [
        {
          'user': 'user-1',
          'answers': [
            {
              'block': 'block-2',
              'value': {
                firstName: 'Ron',
                lastName: 'Steve'
              }
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'Yes'
            },
            {
              'block': 'block-6',
              'value': 'No'
            },
            {
              'block': 'block-7',
              'value': 'Waffles'
            },
            {
              'block': 'block-8',
              'value': 'Burger'
            },
            {
              'block': 'block-9',
              'value': 'Steak'
            }
          ]
        },
        {
          'user': 'user-2',
          'answers': [
            {
              'block': 'block-2',
              'value': 'Jerry'
            },
            {
              'block': 'block-3',
              'value': 'Perdue'
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'Yes'
            },
            {
              'block': 'block-6',
              'value': 'Yes'
            },
            {
              'block': 'block-7',
              'value': 'Pancakes'
            },
            {
              'block': 'block-8',
              'value': 'Sandwich'
            },
            {
              'block': 'block-9',
              'value': 'Shrimp'
            }
          ]
        },
        {
          'user': 'user-3',
          'answers': [
            {
              'block': 'block-2',
              'value': 'Tom'
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'No'
            },
            {
              'block': 'block-6',
              'value': 'Yes'
            },
            {
              'block': 'block-7',
              'value': 'Omelettes'
            },
            {
              'block': 'block-8',
              'value': 'Soup'
            },
            {
              'block': 'block-9',
              'value': 'Lobster'
            }
          ]
        }
      ]
    };

    var conferences = [
      {
        'id': '012',
        'name': 'A Sweet Fall Retreat',
        'landingPage': {
          'blocks': [
            {
              'id': 'landingpage-1',
              'title': 'Location',
              'type': 'paragraphContent',
              'content': '123 Main St. Orlando, FL, 32828'
            },
            {
              'id': 'landingpage-2',
              'title': 'Registration Begins',
              'type': 'paragraphContent',
              'content': 'August 19, 2013 12:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Registration Ends',
              'type': 'paragraphContent',
              'content': 'October 12, 2013 3:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Fall Retreat Starts',
              'type': 'paragraphContent',
              'content': 'October 18, 2013 6:00 PM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Fall Retreat Ends',
              'type': 'paragraphContent',
              'content': 'October 20, 2013 10:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Contact Info',
              'type': 'paragraphContent',
              'content': 'John Smith <john.smith@example.com> 555-555-5555'
            }
          ]
        },
        'pages': [
          {
            'id': '1',
            'title': 'About You',
            'blocks': [
              {
                'id': 'block-1',
                'title': 'Important Information',
                'type': 'paragraphContent',
                'content': 'We are glad you are coming to Fall Retreat!'
              },
              {
                'id': 'block-2',
                'title': 'What\'s your name?',
                'required': true,
                'type': 'nameQuestion'
              },
              {
                'id': 'block-3',
                'title': 'What school do you currently attend?',
                'type': 'textQuestion'
              },
              {
                'id': 'block-4',
                'title': 'Man or Lady?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Man',
                  'Lady'
                ]
              }
            ]
          },
          {
            'id': '2',
            'title': 'Rides',
            'blocks': [
              {
                'id': 'block-1',
                'title': 'Ride Situation',
                'type': 'paragraphContent',
                'content': 'If you are driving, please give someone a ride.'
              },
              {
                'id': 'block-5',
                'title': 'Do you have a car?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Yes',
                  'No'
                ]
              },
              {
                'id': 'block-6',
                'title': 'Do you need a ride?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Yes',
                  'No'
                ]
              }
            ]
          },
          {
            'id': '3',
            'title': 'Food',
            'blocks': [
              {
                'id': 'block-7',
                'title': 'What do you want to eat for breakfast?',
                'type': 'checkboxQuestion',
                'required': true,
                'choices': [
                  'Pancakes',
                  'Waffles',
                  'Omelettes'
                ]
              },
              {
                'id': 'block-8',
                'title': 'What do you want to eat for lunch?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Sandwich',
                  'Soup',
                  'Burger'
                ]
              },
              {
                'id': 'block-9',
                'title': 'What do you want to eat for dinner?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Steak',
                  'Shrimp',
                  'Lobster'
                ]
              }
            ]
          }
        ]
      },
      {
        'id': '123',
        'name': 'Fall Retreat WOOO'
      },
      {
        'id': '234',
        'name': 'Fall Retreat!'
      },
      {
        'id': '345',
        'name': 'Yet Another Fall Retreat (YAFR)'
      },
      {
        'id': '456',
        'name': 'Fall Retreat Is Never Gonna Give You Up'
      }
    ];

    $httpBackend.whenGET('conferences').respond(function () {
      console.log(arguments);
      var headers = {};
      return [200, conferences, headers];
    });
    $httpBackend.whenPOST('conferences').respond(function (verb, url, data) {
      console.log(arguments);

      var conference = angular.extend(angular.fromJson(data), { id: uuid() });

      var headers = {
        'Location': '/conferences/' + conference.id
      };
      return [201, conference, headers];
    });
    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var conference = _.find(conferences, function (conference) {
        return angular.equals(conference.id, conferenceId);
      });

      return [200, conference, {}];
    });
    $httpBackend.whenPUT(/^conferences\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url, data) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var conference = _.find(conferences, function (conference) {
        return angular.equals(conference.id, conferenceId);
      });

      angular.extend(conference, angular.fromJson(data));

      return [200, conference, {}];
    });

    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/registrations\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      return [200, registrations[conferenceId], {}];
    });
    $httpBackend.whenPOST(/^conferences\/[-a-zA-Z0-9]+\/registrations\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var registration = {
        id: uuid(),
        conference: conferenceId,
        answers: []
      };

      var headers = {
        'Location': '/registrations/' + registration.id
      };

      var registrationJson = angular.toJson(registration);
      sessionStorage.setItem(headers.Location, registrationJson);
      sessionStorage.setItem('/conferences/' + conferenceId + '/registrations/current', registration.id);

      return [201, registration, headers];
    });
    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/registrations\/current\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var registrationId = sessionStorage.getItem('/conferences/' + conferenceId + '/registrations/current');
      if (registrationId) {
        return [200, sessionStorage.getItem('/registrations/' + registrationId)];
      }

      return [404];
    });
    $httpBackend.whenGET(/^registrations\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var registrationId = url.split('/')[1];
      var registration = sessionStorage.getItem('/registrations/' + registrationId);
      if (registration) {
        return [200, registration];
      }

      return [404];
    });

    $httpBackend.whenPUT(/^answers\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url, data) {
      console.log(arguments);
      var answer = angular.fromJson(data);

      if (!answer.registration) {
        return [400, { message: 'registration must be present' }];
      }
      if (!answer.block) {
        return [400, { message: 'block must be present' }];
      }
      if (!answer.value) {
        return [400, { message: 'value must be present' }];
      }
      if (!answer.id) {
        answer.id = uuid();
      }

      var registration = sessionStorage.getItem('/registrations/' + answer.registration);
      if (registration) {
        var answers = registration.answers;
        var existingAnswerIndex = _.findIndex(answers, { block: answer.block });
        if (existingAnswerIndex !== -1) {
          answers.splice(existingAnswerIndex, 1);
        }
        answers.push(answer);
      }

      return [200, answer];
    });
  });
<commit_msg>Added conference for testing blocks<commit_after>'use strict';

console.log('**********************USING MOCK BACKEND**********************');

angular.module('confRegistrationWebApp')
  .run(function ($httpBackend, uuid) {

    $httpBackend.whenGET(/views\/.*/).passThrough();

    var registrations = {
      '012': [
        {
          'user': 'user-1',
          'answers': [
            {
              'block': 'block-2',
              'value': {
                firstName: 'Ron',
                lastName: 'Steve'
              }
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'Yes'
            },
            {
              'block': 'block-6',
              'value': 'No'
            },
            {
              'block': 'block-7',
              'value': 'Waffles'
            },
            {
              'block': 'block-8',
              'value': 'Burger'
            },
            {
              'block': 'block-9',
              'value': 'Steak'
            }
          ]
        },
        {
          'user': 'user-2',
          'answers': [
            {
              'block': 'block-2',
              'value': 'Jerry'
            },
            {
              'block': 'block-3',
              'value': 'Perdue'
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'Yes'
            },
            {
              'block': 'block-6',
              'value': 'Yes'
            },
            {
              'block': 'block-7',
              'value': 'Pancakes'
            },
            {
              'block': 'block-8',
              'value': 'Sandwich'
            },
            {
              'block': 'block-9',
              'value': 'Shrimp'
            }
          ]
        },
        {
          'user': 'user-3',
          'answers': [
            {
              'block': 'block-2',
              'value': 'Tom'
            },
            {
              'block': 'block-4',
              'value': 'Man'
            },
            {
              'block': 'block-5',
              'value': 'No'
            },
            {
              'block': 'block-6',
              'value': 'Yes'
            },
            {
              'block': 'block-7',
              'value': 'Omelettes'
            },
            {
              'block': 'block-8',
              'value': 'Soup'
            },
            {
              'block': 'block-9',
              'value': 'Lobster'
            }
          ]
        }
      ]
    };

    var conferences = [
      {
        'id': '012aoeu',
        'name': 'A Test Page Conf',
        'pages': [
          {
            'id': '1',
            'title': 'About You',
            'blocks': [
              {
                'id': 'block-1',
                'title': 'Important Information',
                'type': 'paragraphContent',
                'content': 'This is a paragraph of text.'
              },
              {
                'id': 'block-2',
                'title': 'Name Question',
                'type': 'nameQuestion'
              },
              {
                'id': 'block-3',
                'title': 'Text Question',
                'type': 'textQuestion'
              },
              {
                'id': 'block-4',
                'title': 'Radio Question',
                'type': 'radioQuestion',
                'choices': [
                  'Choice 1',
                  'Choice 2',
                  'Choice 3'
                ]
              },
              {
                'id': 'block-4',
                'title': 'Checkbox Question',
                'type': 'checkboxQuestion',
                'choices': [
                  'Choice 1',
                  'Choice 2',
                  'Choice 3'
                ]
              },
              {
                'id': 'block-5',
                'title': 'Email Question',
                'type': 'emailQuestion'
              }
            ]
          }
        ]
      },
      {
        'id': '012',
        'name': 'A Sweet Fall Retreat',
        'landingPage': {
          'blocks': [
            {
              'id': 'landingpage-1',
              'title': 'Location',
              'type': 'paragraphContent',
              'content': '123 Main St. Orlando, FL, 32828'
            },
            {
              'id': 'landingpage-2',
              'title': 'Registration Begins',
              'type': 'paragraphContent',
              'content': 'August 19, 2013 12:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Registration Ends',
              'type': 'paragraphContent',
              'content': 'October 12, 2013 3:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Fall Retreat Starts',
              'type': 'paragraphContent',
              'content': 'October 18, 2013 6:00 PM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Fall Retreat Ends',
              'type': 'paragraphContent',
              'content': 'October 20, 2013 10:00 AM Eastern Time'
            },
            {
              'id': 'landingpage-2',
              'title': 'Contact Info',
              'type': 'paragraphContent',
              'content': 'John Smith <john.smith@example.com> 555-555-5555'
            }
          ]
        },
        'pages': [
          {
            'id': '1',
            'title': 'About You',
            'blocks': [
              {
                'id': 'block-1',
                'title': 'Important Information',
                'type': 'paragraphContent',
                'content': 'We are glad you are coming to Fall Retreat!'
              },
              {
                'id': 'block-2',
                'title': 'What\'s your name?',
                'required': true,
                'type': 'nameQuestion'
              },
              {
                'id': 'block-3',
                'title': 'What school do you currently attend?',
                'type': 'textQuestion'
              },
              {
                'id': 'block-4',
                'title': 'Man or Lady?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Man',
                  'Lady'
                ]
              }
            ]
          },
          {
            'id': '2',
            'title': 'Rides',
            'blocks': [
              {
                'id': 'block-1',
                'title': 'Ride Situation',
                'type': 'paragraphContent',
                'content': 'If you are driving, please give someone a ride.'
              },
              {
                'id': 'block-5',
                'title': 'Do you have a car?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Yes',
                  'No'
                ]
              },
              {
                'id': 'block-6',
                'title': 'Do you need a ride?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Yes',
                  'No'
                ]
              }
            ]
          },
          {
            'id': '3',
            'title': 'Food',
            'blocks': [
              {
                'id': 'block-7',
                'title': 'What do you want to eat for breakfast?',
                'type': 'checkboxQuestion',
                'required': true,
                'choices': [
                  'Pancakes',
                  'Waffles',
                  'Omelettes'
                ]
              },
              {
                'id': 'block-8',
                'title': 'What do you want to eat for lunch?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Sandwich',
                  'Soup',
                  'Burger'
                ]
              },
              {
                'id': 'block-9',
                'title': 'What do you want to eat for dinner?',
                'type': 'radioQuestion',
                'required': true,
                'choices': [
                  'Steak',
                  'Shrimp',
                  'Lobster'
                ]
              }
            ]
          }
        ]
      },
      {
        'id': '123',
        'name': 'Fall Retreat WOOO'
      },
      {
        'id': '234',
        'name': 'Fall Retreat!'
      },
      {
        'id': '345',
        'name': 'Yet Another Fall Retreat (YAFR)'
      },
      {
        'id': '456',
        'name': 'Fall Retreat Is Never Gonna Give You Up'
      }
    ];

    $httpBackend.whenGET('conferences').respond(function () {
      console.log(arguments);
      var headers = {};
      return [200, conferences, headers];
    });
    $httpBackend.whenPOST('conferences').respond(function (verb, url, data) {
      console.log(arguments);

      var conference = angular.extend(angular.fromJson(data), { id: uuid() });

      var headers = {
        'Location': '/conferences/' + conference.id
      };
      return [201, conference, headers];
    });
    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var conference = _.find(conferences, function (conference) {
        return angular.equals(conference.id, conferenceId);
      });

      return [200, conference, {}];
    });
    $httpBackend.whenPUT(/^conferences\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url, data) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var conference = _.find(conferences, function (conference) {
        return angular.equals(conference.id, conferenceId);
      });

      angular.extend(conference, angular.fromJson(data));

      return [200, conference, {}];
    });

    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/registrations\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      return [200, registrations[conferenceId], {}];
    });
    $httpBackend.whenPOST(/^conferences\/[-a-zA-Z0-9]+\/registrations\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var registration = {
        id: uuid(),
        conference: conferenceId,
        answers: []
      };

      var headers = {
        'Location': '/registrations/' + registration.id
      };

      var registrationJson = angular.toJson(registration);
      sessionStorage.setItem(headers.Location, registrationJson);
      sessionStorage.setItem('/conferences/' + conferenceId + '/registrations/current', registration.id);

      return [201, registration, headers];
    });
    $httpBackend.whenGET(/^conferences\/[-a-zA-Z0-9]+\/registrations\/current\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var conferenceId = url.split('/')[1];

      var registrationId = sessionStorage.getItem('/conferences/' + conferenceId + '/registrations/current');
      if (registrationId) {
        return [200, sessionStorage.getItem('/registrations/' + registrationId)];
      }

      return [404];
    });
    $httpBackend.whenGET(/^registrations\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url) {
      console.log(arguments);

      var registrationId = url.split('/')[1];
      var registration = sessionStorage.getItem('/registrations/' + registrationId);
      if (registration) {
        return [200, registration];
      }

      return [404];
    });

    $httpBackend.whenPUT(/^answers\/[-a-zA-Z0-9]+\/?$/).respond(function (verb, url, data) {
      console.log(arguments);
      var answer = angular.fromJson(data);

      if (!answer.registration) {
        return [400, { message: 'registration must be present' }];
      }
      if (!answer.block) {
        return [400, { message: 'block must be present' }];
      }
      if (!answer.value) {
        return [400, { message: 'value must be present' }];
      }
      if (!answer.id) {
        answer.id = uuid();
      }

      var registration = sessionStorage.getItem('/registrations/' + answer.registration);
      if (registration) {
        var answers = registration.answers;
        var existingAnswerIndex = _.findIndex(answers, { block: answer.block });
        if (existingAnswerIndex !== -1) {
          answers.splice(existingAnswerIndex, 1);
        }
        answers.push(answer);
      }

      return [200, answer];
    });
  });
<|endoftext|>