<commit_before>'use strict';

const fs    = require('fs'),
    util    = require('util'),
    yaml    = require('js-yaml'),
    uuid    = require('node-uuid'),
    events  = require('events'),
    Data    = require('./data.js').Data;

const objects_dir =         __dirname + '/../entities/objects/';
const l10n_dir    =         __dirname + '/../l10n/scripts/objects/';
const objects_scripts_dir = __dirname + '/../scripts/objects/';
const ItemUtil = require('./item_util').ItemUtil;
const _ 			 = require('./helpers');

const Items = function ItemsManager() {
	const self = this;
	self.objects    = {};
	self.load_count = {};

	self.getScriptsDir = () => objects_scripts_dir;
	self.getL10nDir    = () => l10n_dir;

	self.load = (verbose, callback) => {

    const log   = message => { if (verbose) util.log(message); };
    const debug = message => { if (verbose) util.debug(message); };

		log("\tExamining object directory - " + objects_dir);
    fs.readdir(objects_dir, (err, files) => {

      // Load any object files
			for (let j in files) {
				const object_file = objects_dir + files[j];

        //TODO: Extract to Data helper method.
				if (!fs.statSync(object_file).isFile()) { continue; }
				if (!object_file.match(/yml$/)) { continue; }

				// parse the object files
        let objectDefinitions = [];
				try {
          const objectFile  = fs.readFileSync(object_file).toString('utf8');
          const objectYaml  = yaml.load(objectFile)
					objectDefinitions = objectDefinitions.concat(objectYaml);
				} catch (e) {
					log("\t\tError loading object - " + object_file + ' - ' + e.message);
					continue;
				}

        // create and load the objects
				objectDefinitions.forEach(object => {
					const validate = ['keywords', 'short_description', 'vnum'];

					for (let v in validate) {
						if (!(validate[v] in object)) {
              throw new ReferenceError('Error loading object in file ' + object + ' - no ' + validate[v] + ' specified')
						}
					}

					// max load for items so we don't have 1000 items in a room due to respawn
          const maxLoadHit = self.load_count[object.vnum] && self.load_count[object.vnum] >= object.load_max;
					if (maxLoadHit) {
						log("\t\tMaxload of " + object.load_max + " hit for object " + object.vnum);
						return;
					}

					const newObject = new Item(object);
					newObject.setUuid(uuid.v4());

					log("\t\tLoaded item [uuid:" + newObject.getUuid() + ', vnum:' + newObject.vnum + ']');
					self.addItem(newObject);
				});

			}

      log("Loading inventories into containers...");
      self.each(item => {
        if (item.inventory) {
          log("Loading inventory [container: " + item.getUuid() + " vnum: " + item.getVnum() + "]");
          self.spawnContainerInventory(item);
        }
      });

			if (callback) { callback(); }
		});

	};

  //TODO: Account for persisted items eventually (uuids rather than vnums)
  self.spawnContainerInventory = (container, config) => {
    const containerVnum = container.getVnum();

    const hydrateContentsByVnum = vnum => {
      const items = self
        .getByVnum(vnum)
        .filter(item => containerVnum === item.getContainer());
      items.forEach(item => item.setContainer(container.getUuid()));
      return items.map(item => item.getUuid());
    }

    const inv = container.getInventory();
    const containerItems = inv && inv.length ?
      inv.map(hydrateContentsByVnum) :
      [];

    const containerInventory = _.flatten(containerItems);
    container.setInventory(containerInventory);
  }

	/**
	 * Add an item and generate a uuid if necessary
	 * @param Item item
	 */
	self.addItem = item => {
		if (!item.getUuid()) {
			item.setUuid(uuid.v4());
		}
		self.objects[item.getUuid()] = item;
		self.load_count[item.vnum] = self.load_count[item.vnum] ? self.load_count[item.vnum] + 1 : 1;
	};

	/**
	 * Gets all instance of an object by vnum
   * //TODO: Consider using this when checking to see if objs should be loaded.
	 * @param int vnum
	 * @return Item
	 */
	self.getByVnum = vnum => self.filter(obj => obj.getVnum() === vnum);

	/**
	 * retrieve an instance of an object by uuid
	 * @param string uid
	 * @return Item
	 */
	self.get = uid => self.objects[uid];

	/**
	 * proxy Array.each
	 * @param function callback
	 */
	self.each = callback => _.values(self.objects).forEach(callback);

  /**
   * proxy Array.find
   * @param function callback
   */
  self.each = callback => _.values(self.objects).find(callback);

  /**
   * proxy Array.filter
   * @param function callback
   */
  self.filter = callback => _.values(self.objects).filter(callback);

}

const Item = function ItemConstructor(config) {
	const self = this;

  // Fields
	self.keywords;
	self.short_description
	self.description;
	self.inventory;     // Player or Npc object that is holding it
	self.npc_held;      // If it's in an inventory is it an NPC's?
	self.room;          // Room that it's in (vnum)
	self.container;     // Itemception (uid)
	self.vnum;
	self.uuid = null;
	self.equipped = false;
	self.script = null;
	self.attributes = {};
  self.prerequisites = {};

	self.init = config => {
		self.short_description = config.short_description || '';
    self.room_description  = config.room_description  || '';
    self.keywords          = config.keywords      || []; // Required
		self.description       = config.description   || '';
    //TODO: Every other class uses .location for the room vnum, right? use .location and .getLocation
		self.room              = config.room          || null;
		self.npc_held          = config.npc_held      || false;
		self.equipped          = config.equipped      || false;
		self.container         = config.container     || null;
		self.uuid              = config.uuid          || null;
		self.vnum              = config.vnum;         // Required
		self.script            = config.script        || null;
		self.attributes        = config.attributes    || {};
    self.prerequisites     = config.prerequisites || {};
    self.holder            = config.holder        || '';

    self.inventory = config.inventory || (self.isContainer() ? [] : null);

    if (self !== null) {
		  Data.loadListeners(config, l10n_dir, objects_scripts_dir, Data.loadBehaviors(config, 'objects/', self));
    }
	};

	/**#@+
	 * Mutators
	 */
	self.getVnum      = ()   => self.vnum;
	self.getInventory = ()   => self.inventory;
	self.isNpcHeld    = ()   => self.npc_held;
  self.getHolder    = ()   => self.holder; // Name/uid of player/npc holding it.
	self.isEquipped   = ()   => self.equipped;
	self.getRoom      = ()   => self.room;
	self.getContainer = ()   => self.container;
	self.getUuid      = ()   => self.uuid;

  self.getAttributes    = ()     => self.attributes    || {};
  self.getPrerequisites = ()     => self.prerequisites || {};
	self.getAttribute     = attr   => self.attributes[attr]    || null;
  self.getPrerequisite  = attr   => self.prerequisites[attr] || null;

	self.setUuid      = uid   => self.uuid      = uid;
	self.setRoom      = room  => self.room      = room;
	self.setInventory = ids   => self.inventory = ids;
	self.setNpcHeld   = held  => self.npc_held  = held;
  self.setHolder    = id    => self.holder    = id;
	self.setContainer = con   => self.container = uid;
	self.setEquipped  = equip => self.equipped  = !!equip;

	self.setAttribute = (attr, val) => self.attributes[attr] = val;
	/**#@-*/

  self.isContainer = () => (self.getAttribute('maxSizeCapacity') && self.getAttribute('maxWeightCapacity'));

	/**
	 * Get the description, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getDescription = () => typeof self.description === 'string' ?
			self.description :
			self.description['en'];

	/**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getShortDesc = () => typeof self.short_description === 'string' ?
			self.short_description :
			self.short_description['en'];

  /**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getRoomDesc = () => typeof self.room_description === 'string' ?
			self.room_description :
			self.room_description['en'] || self.getShortDesc();

	/**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getKeywords = () => Array.isArray(self.keywords) ?
			self.keywords :
      self.keywords['en'] || [];

	/**
	 * check to see if an item has a specific keyword
	 * @param string keyword
	 * @param string locale
	 * @return boolean
	 */
	self.hasKeyword = (keyword, locale) => _.has(self.getKeywords(locale || 'en'), keyword);

  /**
   * Takes the player and sees which prereqs it doesn't meet.
   * @param player Obj
   * @return Strings[] missed prerequisites
   */
  self.checkPrerequisites = player => {
    const missedPrereqs = [];
    const playerAttr = player.getAttributes();
    for (let attr in self.prerequisites) {
      const prereq = self.prerequisites[attr];
      const playerStat = playerAttr[attr];
      if (!playerStat || playerStat < prereq) {
        missedPrereqs.push(attr);
      }
    }
    return missedPrereqs;
  }

  self.addItem = item => {
    item.setContainer(self.getUuid());
    return self.inventory.push(item.getUuid());
  }

  self.removeItem = item => {
		const uid = item.getUuid();
    if (self.inventory.indexOf(uid) > -1) {
      self.inventory = self.inventory.filter(id => uid !== id);
			item.setContainer(null);
      return item;
    }
    return null;
  }

  self.findInInventory = predicate => self.inventory.find(predicate);

	/**
	 * Get weight of all items inside of a container...
	 * And the container.
	 * @return Number weight
	 */

	self.getWeight = items => self.isContainer() ?
				self.getContainerWeight(items) + self.getAttribute('weight') :
				self.getAttribute('weight');

	self.getContainerWeight = items => self.getInventory()
		.reduce((sum, item) => items.get(item).getWeight() + sum, 0);

	self.getRemainingSizeCapacity = items => self.getAttribute('maxSizeCapacity') - self.getSizeOfContents(items);

	self.getSizeOfContents = items => self
		.getInventory()
		.reduce((sum, uid) => {
			const item = items.get(uid);
			return item.getAttribute('size') + sum;
		}, 0);

	/**
	 * Used when persisting a copy of an item to a JSON
   * (right now this only happens if it is in a player's inventory)
	 * @return object
	 */
	self.flatten = () => ({
			uuid:              self.uuid,
			keywords:          self.keywords,
			short_description: self.short_description,
      room_description:  self.room_description,
			description:       self.description,
			inventory:         self.inventory,
      container:         self.container,
			vnum:              self.vnum,
			script:            self.script,
			equipped:          self.equipped,
			attributes:        self.attributes,
      prerequisites:     self.prerequisites
		});

	self.init(config);
};

util.inherits(Item, events.EventEmitter);

exports.Items = Items;
exports.Item  = Item;
<commit_msg>Fix bad ref :neckbeard:<commit_after>'use strict';

const fs    = require('fs'),
    util    = require('util'),
    yaml    = require('js-yaml'),
    uuid    = require('node-uuid'),
    events  = require('events'),
    Data    = require('./data.js').Data;

const objects_dir =         __dirname + '/../entities/objects/';
const l10n_dir    =         __dirname + '/../l10n/scripts/objects/';
const objects_scripts_dir = __dirname + '/../scripts/objects/';
const ItemUtil = require('./item_util').ItemUtil;
const _ 			 = require('./helpers');

const Items = function ItemsManager() {
	const self = this;
	self.objects    = {};
	self.load_count = {};

	self.getScriptsDir = () => objects_scripts_dir;
	self.getL10nDir    = () => l10n_dir;

	self.load = (verbose, callback) => {

    const log   = message => { if (verbose) util.log(message); };
    const debug = message => { if (verbose) util.debug(message); };

		log("\tExamining object directory - " + objects_dir);
    fs.readdir(objects_dir, (err, files) => {

      // Load any object files
			for (let j in files) {
				const object_file = objects_dir + files[j];

        //TODO: Extract to Data helper method.
				if (!fs.statSync(object_file).isFile()) { continue; }
				if (!object_file.match(/yml$/)) { continue; }

				// parse the object files
        let objectDefinitions = [];
				try {
          const objectFile  = fs.readFileSync(object_file).toString('utf8');
          const objectYaml  = yaml.load(objectFile)
					objectDefinitions = objectDefinitions.concat(objectYaml);
				} catch (e) {
					log("\t\tError loading object - " + object_file + ' - ' + e.message);
					continue;
				}

        // create and load the objects
				objectDefinitions.forEach(object => {
					const validate = ['keywords', 'short_description', 'vnum'];

					for (let v in validate) {
						if (!(validate[v] in object)) {
              throw new ReferenceError('Error loading object in file ' + object + ' - no ' + validate[v] + ' specified')
						}
					}

					// max load for items so we don't have 1000 items in a room due to respawn
          const maxLoadHit = self.load_count[object.vnum] && self.load_count[object.vnum] >= object.load_max;
					if (maxLoadHit) {
						log("\t\tMaxload of " + object.load_max + " hit for object " + object.vnum);
						return;
					}

					const newObject = new Item(object);
					newObject.setUuid(uuid.v4());

					log("\t\tLoaded item [uuid:" + newObject.getUuid() + ', vnum:' + newObject.vnum + ']');
					self.addItem(newObject);
				});

			}

      log("Loading inventories into containers...");
      self.each(item => {
        if (item.inventory) {
          log("Loading inventory [container: " + item.getUuid() + " vnum: " + item.getVnum() + "]");
          self.spawnContainerInventory(item);
        }
      });

			if (callback) { callback(); }
		});

	};

  //TODO: Account for persisted items eventually (uuids rather than vnums)
  self.spawnContainerInventory = (container, config) => {
    const containerVnum = container.getVnum();

    const hydrateContentsByVnum = vnum => {
      const items = self
        .getByVnum(vnum)
        .filter(item => containerVnum === item.getContainer());
      items.forEach(item => item.setContainer(container.getUuid()));
      return items.map(item => item.getUuid());
    }

    const inv = container.getInventory();
    const containerItems = inv && inv.length ?
      inv.map(hydrateContentsByVnum) :
      [];

    const containerInventory = _.flatten(containerItems);
    container.setInventory(containerInventory);
  }

	/**
	 * Add an item and generate a uuid if necessary
	 * @param Item item
	 */
	self.addItem = item => {
		if (!item.getUuid()) {
			item.setUuid(uuid.v4());
		}
		self.objects[item.getUuid()] = item;
		self.load_count[item.vnum] = self.load_count[item.vnum] ? self.load_count[item.vnum] + 1 : 1;
	};

	/**
	 * Gets all instance of an object by vnum
   * //TODO: Consider using this when checking to see if objs should be loaded.
	 * @param int vnum
	 * @return Item
	 */
	self.getByVnum = vnum => self.filter(obj => obj.getVnum() === vnum);

	/**
	 * retrieve an instance of an object by uuid
	 * @param string uid
	 * @return Item
	 */
	self.get = uid => self.objects[uid];

	/**
	 * proxy Array.each
	 * @param function callback
	 */
	self.each = callback => _.values(self.objects).forEach(callback);

  /**
   * proxy Array.find
   * @param function callback
   */
  self.each = callback => _.values(self.objects).find(callback);

  /**
   * proxy Array.filter
   * @param function callback
   */
  self.filter = callback => _.values(self.objects).filter(callback);

}

const Item = function ItemConstructor(config) {
	const self = this;

  // Fields
	self.keywords;
	self.short_description
	self.description;
	self.inventory;     // Player or Npc object that is holding it
	self.npc_held;      // If it's in an inventory is it an NPC's?
	self.room;          // Room that it's in (vnum)
	self.container;     // Itemception (uid)
	self.vnum;
	self.uuid = null;
	self.equipped = false;
	self.script = null;
	self.attributes = {};
  self.prerequisites = {};

	self.init = config => {
		self.short_description = config.short_description || '';
    self.room_description  = config.room_description  || '';
    self.keywords          = config.keywords      || []; // Required
		self.description       = config.description   || '';
    //TODO: Every other class uses .location for the room vnum, right? use .location and .getLocation
		self.room              = config.room          || null;
		self.npc_held          = config.npc_held      || false;
		self.equipped          = config.equipped      || false;
		self.container         = config.container     || null;
		self.uuid              = config.uuid          || null;
		self.vnum              = config.vnum;         // Required
		self.script            = config.script        || null;
		self.attributes        = config.attributes    || {};
    self.prerequisites     = config.prerequisites || {};
    self.holder            = config.holder        || '';

    self.inventory = config.inventory || (self.isContainer() ? [] : null);

    if (self !== null) {
		  Data.loadListeners(config, l10n_dir, objects_scripts_dir, Data.loadBehaviors(config, 'objects/', self));
    }
	};

	/**#@+
	 * Mutators
	 */
	self.getVnum      = ()   => self.vnum;
	self.getInventory = ()   => self.inventory;
	self.isNpcHeld    = ()   => self.npc_held;
  self.getHolder    = ()   => self.holder; // Name/uid of player/npc holding it.
	self.isEquipped   = ()   => self.equipped;
	self.getRoom      = ()   => self.room;
	self.getContainer = ()   => self.container;
	self.getUuid      = ()   => self.uuid;

  self.getAttributes    = ()     => self.attributes    || {};
  self.getPrerequisites = ()     => self.prerequisites || {};
	self.getAttribute     = attr   => self.attributes[attr]    || null;
  self.getPrerequisite  = attr   => self.prerequisites[attr] || null;

	self.setUuid      = uid   => self.uuid      = uid;
	self.setRoom      = room  => self.room      = room;
	self.setInventory = ids   => self.inventory = ids;
	self.setNpcHeld   = held  => self.npc_held  = held;
  self.setHolder    = id    => self.holder    = id;
	self.setContainer = uid   => self.container = uid;
	self.setEquipped  = equip => self.equipped  = !!equip;

	self.setAttribute = (attr, val) => self.attributes[attr] = val;
	/**#@-*/

  self.isContainer = () => (self.getAttribute('maxSizeCapacity') && self.getAttribute('maxWeightCapacity'));

	/**
	 * Get the description, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getDescription = () => typeof self.description === 'string' ?
			self.description :
			self.description['en'];

	/**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getShortDesc = () => typeof self.short_description === 'string' ?
			self.short_description :
			self.short_description['en'];

  /**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getRoomDesc = () => typeof self.room_description === 'string' ?
			self.room_description :
			self.room_description['en'] || self.getShortDesc();

	/**
	 * Get the title, localized if possible
	 * @param string locale
	 * @return string
	 */
	self.getKeywords = () => Array.isArray(self.keywords) ?
			self.keywords :
      self.keywords['en'] || [];

	/**
	 * check to see if an item has a specific keyword
	 * @param string keyword
	 * @param string locale
	 * @return boolean
	 */
	self.hasKeyword = (keyword, locale) => _.has(self.getKeywords(locale || 'en'), keyword);

  /**
   * Takes the player and sees which prereqs it doesn't meet.
   * @param player Obj
   * @return Strings[] missed prerequisites
   */
  self.checkPrerequisites = player => {
    const missedPrereqs = [];
    const playerAttr = player.getAttributes();
    for (let attr in self.prerequisites) {
      const prereq = self.prerequisites[attr];
      const playerStat = playerAttr[attr];
      if (!playerStat || playerStat < prereq) {
        missedPrereqs.push(attr);
      }
    }
    return missedPrereqs;
  }

  self.addItem = item => {
    item.setContainer(self.getUuid());
    return self.inventory.push(item.getUuid());
  }

  self.removeItem = item => {
		const uid = item.getUuid();
    if (self.inventory.indexOf(uid) > -1) {
      self.inventory = self.inventory.filter(id => uid !== id);
			item.setContainer(null);
      return item;
    }
    return null;
  }

  self.findInInventory = predicate => self.inventory.find(predicate);

	/**
	 * Get weight of all items inside of a container...
	 * And the container.
	 * @return Number weight
	 */

	self.getWeight = items => self.isContainer() ?
				self.getContainerWeight(items) + self.getAttribute('weight') :
				self.getAttribute('weight');

	self.getContainerWeight = items => self.getInventory()
		.reduce((sum, item) => items.get(item).getWeight() + sum, 0);

	self.getRemainingSizeCapacity = items => self.getAttribute('maxSizeCapacity') - self.getSizeOfContents(items);

	self.getSizeOfContents = items => self
		.getInventory()
		.reduce((sum, uid) => {
			const item = items.get(uid);
			return item.getAttribute('size') + sum;
		}, 0);

	/**
	 * Used when persisting a copy of an item to a JSON
   * (right now this only happens if it is in a player's inventory)
	 * @return object
	 */
	self.flatten = () => ({
			uuid:              self.uuid,
			keywords:          self.keywords,
			short_description: self.short_description,
      room_description:  self.room_description,
			description:       self.description,
			inventory:         self.inventory,
      container:         self.container,
			vnum:              self.vnum,
			script:            self.script,
			equipped:          self.equipped,
			attributes:        self.attributes,
      prerequisites:     self.prerequisites
		});

	self.init(config);
};

util.inherits(Item, events.EventEmitter);

exports.Items = Items;
exports.Item  = Item;
<|endoftext|>


<commit_before>---
layout: null
---

var APP_PREFIX = 'alexdiliberto-cache-';
var VESION = 'v20';
var CACHE_NAME = APP_PREFIX + VESION
var urlsToCache = [];

// Cache posts
// Limits the number of posts that gets cached to 3
// Reads a piece of front-matter in each post that directs the second loop to the folder where the assets are held
// {% for post in site.posts limit:3 %}
//   urlsToCache.push("{{ post.url }}");
//   {% for asset in post.assets %}
//     {% for file in site.static_files %}
//       {% if file.path contains asset %}
//         urlsToCache.push("{{ file.path }}");
//       {% endif %}
//     {% endfor %}
//   {% endfor %}
// {% endfor %}

{% for post in site.posts limit:3 %}
  urlsToCache.push("{{ post.url }}");
{% endfor %}

// Cache pages
{% for page in site.html_pages %}
  {% unless page.url contains '/talks' %}
    urlsToCache.push("{{ page.url }}");
  {% endunless %}
{% endfor %}

// Cache assets
{% for file in site.static_files %}
  {% unless file.path contains '/talks' %}
    {% if file.path contains '/img' or file.extname == '.js' or file.extname == '.png' %}
      urlsToCache.push("{{ file.path }}")
    {% endif %}
  {% endunless %}
{% endfor %}

//console.log('urlsToCache', urlsToCache);

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.match(event.request).then(function(response) {
        return response || fetch(event.request).then(function(response) {
          if (event.request.url.indexOf('chrome-extension') !== -1) {
            return response;
          }
          cache.put(event.request, response.clone());
          return response;
        });
      });
    }).catch(function() {
      return caches.match('/offline/');
    })
  );
});

self.addEventListener('activate', function(event) {
  var cacheWhitelist = [CACHE_NAME];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
<commit_msg>👊 sw cache version<commit_after>---
layout: null
---

var APP_PREFIX = 'alexdiliberto-cache-';
var VESION = 'v21';
var CACHE_NAME = APP_PREFIX + VESION
var urlsToCache = [];

// Cache posts
// Limits the number of posts that gets cached to 3
// Reads a piece of front-matter in each post that directs the second loop to the folder where the assets are held
// {% for post in site.posts limit:3 %}
//   urlsToCache.push("{{ post.url }}");
//   {% for asset in post.assets %}
//     {% for file in site.static_files %}
//       {% if file.path contains asset %}
//         urlsToCache.push("{{ file.path }}");
//       {% endif %}
//     {% endfor %}
//   {% endfor %}
// {% endfor %}

{% for post in site.posts limit:3 %}
  urlsToCache.push("{{ post.url }}");
{% endfor %}

// Cache pages
{% for page in site.html_pages %}
  {% unless page.url contains '/talks' %}
    urlsToCache.push("{{ page.url }}");
  {% endunless %}
{% endfor %}

// Cache assets
{% for file in site.static_files %}
  {% unless file.path contains '/talks' %}
    {% if file.path contains '/img' or file.extname == '.js' or file.extname == '.png' %}
      urlsToCache.push("{{ file.path }}")
    {% endif %}
  {% endunless %}
{% endfor %}

//console.log('urlsToCache', urlsToCache);

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open(CACHE_NAME).then(function(cache) {
      return cache.match(event.request).then(function(response) {
        return response || fetch(event.request).then(function(response) {
          if (event.request.url.indexOf('chrome-extension') !== -1) {
            return response;
          }
          cache.put(event.request, response.clone());
          return response;
        });
      });
    }).catch(function() {
      return caches.match('/offline/');
    })
  );
});

self.addEventListener('activate', function(event) {
  var cacheWhitelist = [CACHE_NAME];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
<|endoftext|>


<commit_before>var useLoc = document.getElementById('use-loc');
useLoc.checked = localStorage.useLoc === 'true';
useLoc.onchange = function() {
  localStorage.useLoc = useLoc.checked;
};

var navBtn = document.querySelector('#nav button');
navBtn.onclick = function() {
  if (localStorage.useLoc !== 'true') {
    navBtn.disabled = true;
    return (location.href="/mobile");
  }
  else if ("geolocation" in navigator) {
    // Getting the GPS position can be very slow on some devices
    navBtn.disabled = true;
    navBtn.innerText = 'Locating...';

    // Get location and use it!
    navigator.geolocation.getCurrentPosition(function(p) {
      navBtn.innerText = 'Reloading...';
      location.href='/mobile?lat='+p.coords.latitude+'&lon='+p.coords.longitude;

    }, function(err) {
      navBtn.innerText = 'Reload';
      navBtn.disabled = false;
      alert('Failed to get location: ' + err.message);

    }, {
      enableHighAccuracy: true,
      timeout: 5000,
      maximumAge: 5000
    });
  }
  else {
    alert('Your device does not support web geolocation');
  }
};

function updateTimes() {
  // server tells us how many seconds are left we note the
  // pageload time and count down from there.
  // Yes, this could be a smidge innaccurate, but not by
  // more than 1 second or so which doesn't matter.
  // And now we don't have to deal with timestamps and dates!
  var remains = document.querySelectorAll('div.remain');
  for (var i = 0; i < remains.length; ++i) {
    var element = remains[i];
    var now = new Date().getTime();
    var secondsPassed = Math.floor((now - page_loaded)/1000);
    var alivefor =  element.getAttribute('disappear');
    var remain = alivefor - secondsPassed;
    var min = Math.floor(remain / 60);
    var sec = remain % 60;
    element.innerText = (remain > 0) ? min + ' min ' + sec + ' sec' : '(expired)';
  }
};
setInterval(updateTimes, 1000);

document.querySelectorAll("li").forEach(function(listItem) {
  listItem.onclick = function() {
    window.document.location = this.getAttribute('href');
  };
});
<commit_msg>Fix absolute paths in mobile.js. (#2772)<commit_after>var useLoc = document.getElementById('use-loc');
useLoc.checked = localStorage.useLoc === 'true';
useLoc.onchange = function() {
  localStorage.useLoc = useLoc.checked;
};

var navBtn = document.querySelector('#nav button');
navBtn.onclick = function() {
  if (localStorage.useLoc !== 'true') {
    navBtn.disabled = true;
    return (location.href="mobile");
  }
  else if ("geolocation" in navigator) {
    // Getting the GPS position can be very slow on some devices
    navBtn.disabled = true;
    navBtn.innerText = 'Locating...';

    // Get location and use it!
    navigator.geolocation.getCurrentPosition(function(p) {
      navBtn.innerText = 'Reloading...';
      location.href='mobile?lat='+p.coords.latitude+'&lon='+p.coords.longitude;

    }, function(err) {
      navBtn.innerText = 'Reload';
      navBtn.disabled = false;
      alert('Failed to get location: ' + err.message);

    }, {
      enableHighAccuracy: true,
      timeout: 5000,
      maximumAge: 5000
    });
  }
  else {
    alert('Your device does not support web geolocation');
  }
};

function updateTimes() {
  // server tells us how many seconds are left we note the
  // pageload time and count down from there.
  // Yes, this could be a smidge innaccurate, but not by
  // more than 1 second or so which doesn't matter.
  // And now we don't have to deal with timestamps and dates!
  var remains = document.querySelectorAll('div.remain');
  for (var i = 0; i < remains.length; ++i) {
    var element = remains[i];
    var now = new Date().getTime();
    var secondsPassed = Math.floor((now - page_loaded)/1000);
    var alivefor =  element.getAttribute('disappear');
    var remain = alivefor - secondsPassed;
    var min = Math.floor(remain / 60);
    var sec = remain % 60;
    element.innerText = (remain > 0) ? min + ' min ' + sec + ' sec' : '(expired)';
  }
};
setInterval(updateTimes, 1000);

document.querySelectorAll("li").forEach(function(listItem) {
  listItem.onclick = function() {
    window.document.location = this.getAttribute('href');
  };
});
<|endoftext|>


<commit_before><commit_msg>sw.js<commit_after><|endoftext|>


<commit_before>// This should be called once per download
// Each event has a dataChannel that the data will be piped throught
self.onmessage = event => {
    // Create a uniq link for the download
    let uniqLink = 'intercept-me-nr' + Math.random()

    let p = new Promise((resolve, reject) => {
        let stream = createStream(resolve, reject, event.ports[0])
        console.log(stream)
        hijacke(uniqLink, stream, event.data)
    })

	// Tell the middle man to open the link to kick start the stream download
	clients.matchAll({includeUncontrolled: true, type: 'window'}).then(clients => {
		clients[0].postMessage({
	        href: uniqLink
	    })
	})

	return

    // Beginning in Chrome 51, event is an ExtendableMessageEvent, which supports
    // the waitUntil() method for extending the lifetime of the event handler
    // until the promise is resolved.
    if ('waitUntil' in event) {
        event.waitUntil(p)
    }

    // Without support for waitUntil(), there's a chance that if the promise chain
    // takes "too long" to execute, the service worker might be automatically
    // stopped before it's complete.
}

function createStream(resolve, reject, port){
    // ReadableStream is only supported by chrome 52, but can be enabled
    // with a flag chrome://flags/#enable-experimental-web-platform-features
    return new ReadableStream({
		start(controller) {
			port.postMessage("ready")
			// When we recive data on the messageChannel, we write
			port.onmessage = event => {
				console.log("write: ", event.data)
				// We finaly have a abortable stream =D
				if(event.data === 'end')
					return controller.close()

				controller.enqueue(event.data)
			}
		},
		cancel() {
			console.log("user aborted")
		}
	})
}

function hijacke(uniqLink, stream, filename){
	let listener

    self.addEventListener('fetch', listener = event => {
    	console.log("handeling fetch event for", event.request.url)
        if(!event.request.url.includes(uniqLink))
    		return

        self.removeEventListener('fetch', listener)

    	let res = new Response(stream, {
    		headers: {
				'Content-Disposition': 'attachment; filename=' + filename
    		}
    	})
		console.log(res)
    	event.respondWith(res)
    })
}
<commit_msg>broadcast?<commit_after>// This should be called once per download
// Each event has a dataChannel that the data will be piped throught
self.onmessage = event => {
    // Create a uniq link for the download
    let uniqLink = 'intercept-me-nr' + Math.random()

    let p = new Promise((resolve, reject) => {
        let stream = createStream(resolve, reject, event.ports[0])
        console.log(stream)
        hijacke(uniqLink, stream, event.data)
    })

	// Tell the middle man to open the link to kick start the stream download
	clients.matchAll({includeUncontrolled: true, type: 'window'}).then(clients => {
		for(let client of clients){
			client.postMessage({
		        href: uniqLink
		    })
		}
	})

	return

    // Beginning in Chrome 51, event is an ExtendableMessageEvent, which supports
    // the waitUntil() method for extending the lifetime of the event handler
    // until the promise is resolved.
    if ('waitUntil' in event) {
        event.waitUntil(p)
    }

    // Without support for waitUntil(), there's a chance that if the promise chain
    // takes "too long" to execute, the service worker might be automatically
    // stopped before it's complete.
}

function createStream(resolve, reject, port){
    // ReadableStream is only supported by chrome 52, but can be enabled
    // with a flag chrome://flags/#enable-experimental-web-platform-features
    return new ReadableStream({
		start(controller) {
			port.postMessage("ready")
			// When we recive data on the messageChannel, we write
			port.onmessage = event => {
				console.log("write: ", event.data)
				// We finaly have a abortable stream =D
				if(event.data === 'end')
					return controller.close()

				controller.enqueue(event.data)
			}
		},
		cancel() {
			console.log("user aborted")
		}
	})
}

function hijacke(uniqLink, stream, filename){
	let listener

    self.addEventListener('fetch', listener = event => {
    	console.log("handeling fetch event for", event.request.url)
        if(!event.request.url.includes(uniqLink))
    		return

        self.removeEventListener('fetch', listener)

    	let res = new Response(stream, {
    		headers: {
				'Content-Disposition': 'attachment; filename=' + filename
    		}
    	})
		console.log(res)
    	event.respondWith(res)
    })
}
<|endoftext|>


<commit_before>/* ===========================================================
 * sw.js
 * ===========================================================
 * Copyright 2016 @huxpro
 * Licensed under Apache 2.0
 * Register service worker.
 * ========================================================== */

const PRECACHE = 'precache-v1';
const RUNTIME = 'runtime';
const HOSTNAME_WHITELIST = [
  self.location.hostname,
  "huangxuan.me",
  "yanshuo.io",
  "cdnjs.cloudflare.com"
]

// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
  var now = Date.now();
  url = new URL(req.url)

  // 1. fixed http URL
  // Just keep syncing with location.protocol
  // fetch(httpURL) belongs to active mixed content.
  // And fetch(httpRequest) is not supported yet.
  url.protocol = self.location.protocol

  // 2. add query for caching-busting.
  // Github Pages served with Cache-Control: max-age=600
  // max-age on mutable content is error-prone, with SW life of bugs can even extend.
  // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
  // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
  url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
  return url.href
}

// The Util Function to detect and polyfill req.mode="navigate"
// request.mode of 'navigate' is unfortunately not supported in Chrome
// versions older than 49, so we need to include a less precise fallback,
// which checks for a GET request with an Accept: text/html header.
const isNavigationReq = (req) => (req.mode === 'navigate' || (req.method === 'GET' && req.headers.get('accept').includes('text/html')))

// The Util Function to detect if a req is end with extension
// Accordin to Fetch API spec <https://fetch.spec.whatwg.org/#concept-request-destination>
// Any HTML's navigation has consistently mode="navigate" type="" and destination="document"
// including requesting an img (or any static resources) from URL Bar directly.
// So It ends up with that regExp is still the king of URL routing ;)
// P.S. An url.pathname has no '.' can not indicate it ends with extension (e.g. /api/version/1.2/)
const endWithExtension = (req) => Boolean(new URL(req.url).pathname.match(/\.\w+$/))

// Redirect in SW manually fixed github pages arbitray 404s on things?blah
// what we want:
//    repo?blah -> !(gh 404) -> sw 302 -> repo/?blah
//    .ext?blah -> !(sw 302 -> .ext/?blah -> gh 404) -> .ext?blah
// If It's a navigation req and it's url.pathname isn't end with '/' or '.ext'
// it should be a dir/repo request and need to be fixed (a.k.a be redirected)
// Tracking https://twitter.com/Huxpro/status/798816417097224193
const shouldRedirect = (req) => (isNavigationReq(req) && new URL(req.url).pathname.substr(-1) !== "/" && !endWithExtension(req))

// The Util Function to get redirect URL
// `${url}/` would mis-add "/" in the end of query, so we use URL object.
// P.P.S. Always trust url.pathname instead of the whole url string.
const getRedirectUrl = (req) => {
  url = new URL(req.url)
  url.pathname += "/"
  return url.href
}

/**
 *  @Lifecycle Install
 *  Precache anything static to this version of your app.
 *  e.g. App Shell, 404, JS/CSS dependencies...
 *
 *  waitUntil() : installing ====> installed
 *  skipWaiting() : waiting(installed) ====> activating
 */
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(PRECACHE).then(cache => {
      return cache.add('offline.html')
      .then(self.skipWaiting())
      .catch(err => console.log(err))
    })
  )
});

/**
 *  @Lifecycle Activate
 *  New one activated when old isnt being used.
 *
 *  waitUntil(): activating ====> activated
 */
self.addEventListener('activate',  event => {
  console.log('service worker activated.')
  event.waitUntil(self.clients.claim());
});

/**
 *  @Functional Fetch
 *  All network requests are being intercepted here.
 *
 *  void respondWith(Promise<Response> r);
 */
self.addEventListener('fetch', event => {
  // logs for debugging
  console.log(`fetch ${event.request.url}`)
  //console.log(` - type: ${event.request.type}; destination: ${event.request.destination}`)
  //console.log(` - mode: ${event.request.mode}, accept: ${event.request.headers.get('accept')}`)

  // Skip some of cross-origin requests, like those for Google Analytics.
  if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {

    // Redirect in SW manually fixed github pages 404s on repo?blah
    if(shouldRedirect(event.request)){
      event.respondWith(Response.redirect(getRedirectUrl(event.request)))
      return;
    }

    // Stale-while-revalidate
    // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
    // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
    const cached = caches.match(event.request);
    const fixedUrl = getFixedUrl(event.request);
    const fetched = fetch(fixedUrl, {cache: "no-store"});
    const fetchedCopy = fetched.then(resp => resp.clone());

    // Call respondWith() with whatever we get first.
    // If the fetch fails (e.g disconnected), wait for the cache.
    // If there’s nothing in cache, wait for the fetch.
    // If neither yields a response, return offline pages.
    event.respondWith(
      Promise.race([fetched.catch(_ => cached), cached])
        .then(resp => resp || fetched)
        .catch(_ => caches.match('offline.html'))
    );

    // Update the cache with the version we fetched (only for ok status)
    event.waitUntil(
      Promise.all([fetchedCopy, caches.open(RUNTIME)])
        .then(([response, cache]) => response.ok && cache.put(event.request, response))
        .catch(_ => {/* eat any errors */})
    );
  }
});
<commit_msg>update sw.js<commit_after>/* ===========================================================
 * sw.js
 * ===========================================================
 * Copyright 2016 @huxpro
 * Licensed under Apache 2.0
 * service worker scripting
 * ========================================================== */

// CACHE_NAMESPACE
// CacheStorage is shared between all sites under same domain.
// A namespace can prevent potential name conflicts and mis-deletion.
const CACHE_NAMESPACE = 'main-'

const CACHE = CACHE_NAMESPACE + 'precache-then-runtime';
const PRECACHE_LIST = [
  "./",
  "./offline.html",
  "./js/jquery.min.js",
  "./js/bootstrap.min.js",
  "./js/hux-blog.min.js",
  "./js/snackbar.js",
  "./img/icon_wechat.png",
  "./img/avatar-hux.jpg",
  "./img/home-bg.jpg",
  "./img/404-bg.jpg",
  "./css/hux-blog.min.css",
  "./css/syntax.css",
  "./css/bootstrap.min.css"
  // "//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css",
  // "//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/fonts/fontawesome-webfont.woff2?v=4.6.3",
  // "//cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.6/fastclick.min.js"
]
const HOSTNAME_WHITELIST = [
  self.location.hostname,
  "huangxuan.me",
  "yanshuo.io",
  "cdnjs.cloudflare.com"
]
const DEPRECATED_CACHES = ['precache-v1', 'runtime', 'main-precache-v1', 'main-runtime']

// The Util Function to hack URLs of intercepted requests
const getCacheBustingUrl = (req) => {
  var now = Date.now();
  url = new URL(req.url)

  // 1. fixed http URL
  // Just keep syncing with location.protocol
  // fetch(httpURL) belongs to active mixed content.
  // And fetch(httpRequest) is not supported yet.
  url.protocol = self.location.protocol

  // 2. add query for caching-busting.
  // Github Pages served with Cache-Control: max-age=600
  // max-age on mutable content is error-prone, with SW life of bugs can even extend.
  // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
  // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
  url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
  return url.href
}

// The Util Function to detect and polyfill req.mode="navigate"
// request.mode of 'navigate' is unfortunately not supported in Chrome
// versions older than 49, so we need to include a less precise fallback,
// which checks for a GET request with an Accept: text/html header.
const isNavigationReq = (req) => (req.mode === 'navigate' || (req.method === 'GET' && req.headers.get('accept').includes('text/html')))

// The Util Function to detect if a req is end with extension
// Accordin to Fetch API spec <https://fetch.spec.whatwg.org/#concept-request-destination>
// Any HTML's navigation has consistently mode="navigate" type="" and destination="document"
// including requesting an img (or any static resources) from URL Bar directly.
// So It ends up with that regExp is still the king of URL routing ;)
// P.S. An url.pathname has no '.' can not indicate it ends with extension (e.g. /api/version/1.2/)
const endWithExtension = (req) => Boolean(new URL(req.url).pathname.match(/\.\w+$/))

// Redirect in SW manually fixed github pages arbitray 404s on things?blah
// what we want:
//    repo?blah -> !(gh 404) -> sw 302 -> repo/?blah
//    .ext?blah -> !(sw 302 -> .ext/?blah -> gh 404) -> .ext?blah
// If It's a navigation req and it's url.pathname isn't end with '/' or '.ext'
// it should be a dir/repo request and need to be fixed (a.k.a be redirected)
// Tracking https://twitter.com/Huxpro/status/798816417097224193
const shouldRedirect = (req) => (isNavigationReq(req) && new URL(req.url).pathname.substr(-1) !== "/" && !endWithExtension(req))

// The Util Function to get redirect URL
// `${url}/` would mis-add "/" in the end of query, so we use URL object.
// P.P.S. Always trust url.pathname instead of the whole url string.
const getRedirectUrl = (req) => {
  url = new URL(req.url)
  url.pathname += "/"
  return url.href
}

/**
 *  @Lifecycle Install
 *  Precache anything static to this version of your app.
 *  e.g. App Shell, 404, JS/CSS dependencies...
 *
 *  waitUntil() : installing ====> installed
 *  skipWaiting() : waiting(installed) ====> activating
 */
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open(CACHE).then(cache => {
      return cache.addAll(PRECACHE_LIST)
        .then(self.skipWaiting())
        .catch(err => console.log(err))
    })
  )
});

/**
 *  @Lifecycle Activate
 *  New one activated when old isnt being used.
 *
 *  waitUntil(): activating ====> activated
 */
self.addEventListener('activate', event => {
  // delete old deprecated caches.
  caches.keys().then(cacheNames => Promise.all(
    cacheNames
      .filter(cacheName => DEPRECATED_CACHES.includes(cacheName))
      .map(cacheName => caches.delete(cacheName))
  ))
  console.log('service worker activated.')
  event.waitUntil(self.clients.claim());
});

var fetchHelper = {

  fetchThenCache: function(request){
    // Requests with mode "no-cors" can result in Opaque Response,
    // Requests to Allow-Control-Cross-Origin: * can't include credentials.
    const init = { mode: "cors", credentials: "omit" }

    const fetched = fetch(request, init)
    const fetchedCopy = fetched.then(resp => resp.clone());

    // NOTE: Opaque Responses have no hedaders so [[ok]] make no sense to them
    //       so Opaque Resp will not be cached in this case.
    Promise.all([fetchedCopy, caches.open(CACHE)])
      .then(([response, cache]) => response.ok && cache.put(request, response))
      .catch(_ => {/* eat any errors */})

    return fetched;
  },

  cacheFirst: function(url){
    return caches.match(url)
      .then(resp => resp || this.fetchThenCache(url))
      .catch(_ => {/* eat any errors */})
  }
}

/**
 *  @Functional Fetch
 *  All network requests are being intercepted here.
 *
 *  void respondWith(Promise<Response> r);
 */
self.addEventListener('fetch', event => {
  // logs for debugging
  //console.log(`fetch ${event.request.url}`)
  //console.log(` - type: ${event.request.type}; destination: ${event.request.destination}`)
  //console.log(` - mode: ${event.request.mode}, accept: ${event.request.headers.get('accept')}`)

  // Skip some of cross-origin requests, like those for Google Analytics.
  if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {

    // Redirect in SW manually fixed github pages 404s on repo?blah
    if (shouldRedirect(event.request)) {
      event.respondWith(Response.redirect(getRedirectUrl(event.request)))
      return;
    }

    // Cache-only Startgies for ys.static resources
    if (event.request.url.indexOf('ys.static') > -1){
      event.respondWith(fetchHelper.cacheFirst(event.request.url))
      return;
    }

    // Stale-while-revalidate for possiblily dynamic content
    // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
    // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
    const cached = caches.match(event.request);
    const fetched = fetch(getCacheBustingUrl(event.request), { cache: "no-store" });
    const fetchedCopy = fetched.then(resp => resp.clone());

    // Call respondWith() with whatever we get first.
    // Promise.race() resolves with first one settled (even rejected)
    // If the fetch fails (e.g disconnected), wait for the cache.
    // If there’s nothing in cache, wait for the fetch.
    // If neither yields a response, return offline pages.
    event.respondWith(
      Promise.race([fetched.catch(_ => cached), cached])
        .then(resp => resp || fetched)
        .catch(_ => caches.match('offline.html'))
    );

    // Update the cache with the version we fetched (only for ok status)
    event.waitUntil(
      Promise.all([fetchedCopy, caches.open(CACHE)])
        .then(([response, cache]) => response.ok && cache.put(event.request, response))
        .catch(_ => {/* eat any errors */ })
    );

    // If one request is a HTML naviagtion, checking update!
    if (isNavigationReq(event.request)) {
      // you need "preserve logs" to see this log
      // cuz it happened before navigating
      console.log(`fetch ${event.request.url}`)
      event.waitUntil(revalidateContent(cached, fetchedCopy))
    }
  }
});

/**
 * Broadcasting all clients with MessageChannel API
 */
function sendMessageToAllClients(msg) {
  self.clients.matchAll().then(clients => {
    clients.forEach(client => {
      console.log(client);
      client.postMessage(msg)
    })
  })
}

/**
 * Broadcasting all clients async
 */
function sendMessageToClientsAsync(msg) {
  // waiting for new client alive with "async" setTimeout hacking
  // https://twitter.com/Huxpro/status/799265578443751424
  // https://jakearchibald.com/2016/service-worker-meeting-notes/#fetch-event-clients
  setTimeout(() => {
    sendMessageToAllClients(msg)
  }, 1000)
}

/**
 * if content modified, we can notify clients to refresh
 * TODO: Gh-pages rebuild everything in each release. should find a workaround (e.g. ETag with cloudflare)
 *
 * @param  {Promise<response>} cachedResp  [description]
 * @param  {Promise<response>} fetchedResp [description]
 * @return {Promise}
 */
function revalidateContent(cachedResp, fetchedResp) {
  // revalidate when both promise resolved
  return Promise.all([cachedResp, fetchedResp])
    .then(([cached, fetched]) => {
      const cachedVer = cached.headers.get('last-modified')
      const fetchedVer = fetched.headers.get('last-modified')
      console.log(`"${cachedVer}" vs. "${fetchedVer}"`);
      if (cachedVer !== fetchedVer) {
        sendMessageToClientsAsync({
          'command': 'UPDATE_FOUND',
          'url': fetched.url
        })
      }
    })
    .catch(err => console.log(err))
}
<|endoftext|>


<commit_before>/*!
    Kavie - knockout observable validator
    Author: Matthew Nitschke
    License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/

// This is the knockout js extender
// simply adds a few things to the observable so we can access these from the kavie object
ko.extenders.kavie = function (target, rules) {
    target.hasError = ko.observable(); // tracks whether this observable is valid or not

    // if contains addToArray, add this observable to the global Kavie object
    // (not sure if i like this)
    if (rules.addToArray){
      Kavie.add(target);
      delete rules.addToArray;
    }

    // if section exsists add observable to it
    if (rules.section){
      if (!Kavie.sections[rules.section]){
        Kavie.sections[rules.section] = [];
      }

      Kavie.sections[rules.section].push(target);

      delete rules.section;
    }

    // add the passed in rules to the observable
    target.rules = rules;

    // Simply checks each rule attached to this observable and changes hasError variable
    function validate(newValue) {
        var rules = target.rules;

        for (key in rules) {
            for (funcKey in Kavie.validatorFunctions) {
                if (key == funcKey) {
                    var isValid = Kavie.validatorFunctions[funcKey](rules[key], newValue);
                    if (!isValid) {
                        target.hasError(true);
                        return;
                    }
                }
            }
        }

        target.hasError(false);
    }

    target.startValidation = function () {
        target.subscription = target.subscribe(validate); // creates a subscribable to update when value changes
        validate(target());
    }

    target.stopValidation = function () {
        target.subscription.dispose();
        target.hasError(false);
    }

    return target;
};

var Kavie = function(){} // the global kavie object
Kavie.observables = [];
Kavie.sections = [];

// a simple helper function to check if observables have been extended with the kavie extender
Kavie.isKavieObservable = function(observable){
  if (observable.hasOwnProperty("hasError")){ // when you extend an observable with kavie, it addes hasError.
    return true;
  } else {
    return false;
  }
}

// adds a kavie observable to the global kavie object
Kavie.add = function(obs){
  if (this.isKavieObservable(obs)){
    this.observables = this.observables.concat(obs);
  }
}

// returs a list of all the kavie observables
// pulls from the global object, and a viewModel or section that can be passed in
Kavie.compileObservables = function(vm){
  var kavieObservables = [];

  if (this.observables.length > 0){
    kavieObservables = kavieObservables.concat(this.observables);
  }
  if (vm){
    var keys = Object.keys(vm);
    for(var i = 0; i < keys.length; i ++){
      if (this.isKavieObservable(vm[keys[i]])){
        kavieObservables.push(vm[keys[i]]);
      }
    }
  }
  return kavieObservables;
}

// the main is valid function run to see if observables are valid
Kavie.isValid = function(vm){
  var isValid = true;

  var kavieObservables = Kavie.compileObservables(vm);

  for(var i = 0; i < kavieObservables.length; i ++){
    kavieObservables[i].startValidation();

    if (isValid) {
        if (kavieObservables[i].hasError()) {
            isValid = false;
        }
    }
  }
  return isValid;
}

// simply deactivates all observables
Kavie.deactivate = function(vm){
  var kavieObservables = Kavie.compileObservables(vm);

  for(var i = 0; i < kavieObservables.length; i ++){
    kavieObservables[i].stopValidation();
  }
}

// built in validator functions
Kavie.validatorFunctions = {
    required: function (propVal, eleVal) {
        if (propVal) {
            return (eleVal ? true : false);
        } else {
            return true;
        }
    },
    numeric: function(propVal, eleVal){
      if (propVal){
         return !isNaN(parseFloat(eleVal)) && isFinite(eleVal);
      } else {
        return true;
      }

    },
    max: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length <= propVal;
      }
      return true; // if no value is found, it doesnt have a length. So thus it is less than the propVal
    },
    min: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length >= propVal;
      }
      return false; // opposite from above
    },
    date: function (propVal, eleVal) {
      if (eleVal){
        if (eleVal.length == 10) {
            if (new Date(eleVal) == "Invalid Date") {
                return false;
            }
            return true;
        }
        return false;
      }
      return false; // noting is an invalid date
    },
    birthdate: function (propVal, eleVal) {
        // check to see if it is a valid date
        if (!Kavie.validatorFunctions.date(propVal, eleVal)) {
            return false;
        }

        var date = new Date(eleVal);

        // check to see if the date is in the future
        if (date > new Date()) {
            return false;
        }

        // check to see if date is a rational birthdate
        var minDateAllowed = new Date();
        minDateAllowed.setFullYear(minDateAllowed.getFullYear() - 120); // 120 is age of oldest person allowd

        if (date < minDateAllowed) {
            return false;
        }

        return true;
    },
    regexPattern: function(propVal, eleVal){
      if (eleVal){
        return eleVal.toString().match(propVal) !== null;
      } else {
        return false;
      }
    }
}
<commit_msg>Implemented singleton pattern<commit_after>/*!
    Kavie - knockout observable validator
    Author: Matthew Nitschke
    License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/

// This is a singleton pattern for the Kavie object to validate against
;(function(ns){

  // holds out observables and sections
  ns.observables = [];
  ns.sections = [];

  // adds a kavieObservables to the object observables
  ns.add = function(obs){
    if (isKavieObservable(obs)){
      observables = this.observables.concat(obs);
    }
  }

  // turn validaton on
  ns.isValid = function(vm){
    // vm can be a viewModel or a Kavie.section
    var isValid = true;

    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].startValidation();

      if (isValid) {
          if (kavieObservables[i].hasError()) {
              isValid = false;
          }
      }
    }
    return isValid;
  }

  // turns off validation
  ns.deactivate = function(vm){
    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].stopValidation();
    }
  }

  // simple helper method to see if an observable has been extended with the kavie extender
  var isKavieObservable = function(observable){
    if (observable.hasOwnProperty("hasError")){ // when you extend an observable with kavie, it addes hasError.
      return true;
    } else {
      return false;
    }
  }

  // returns an array of all kavieObservables found in the viewModel potentially passed in,
  // and attached to the Kavie object it's self
  var compileObservables = function(vm){
    var kavieObservables = [];

    if (ns.observables.length > 0){
      kavieObservables = kavieObservables.concat(this.observables);
    }

    // vm can be a viewModel or a Kavie.section['...']
    if (vm){
      var keys = Object.keys(vm);
      for(var i = 0; i < keys.length; i ++){
        if (isKavieObservable(vm[keys[i]])){
          kavieObservables.push(vm[keys[i]]);
        }
      }
    }
    return kavieObservables;
  }

  // built in validator functions
  ns.validatorFunctions = {
    required: function (propVal, eleVal) {
        if (propVal) {
            return (eleVal ? true : false);
        } else {
            return true;
        }
    },
    numeric: function(propVal, eleVal){
      if (propVal){
         return !isNaN(parseFloat(eleVal)) && isFinite(eleVal);
      } else {
        return true;
      }

    },
    max: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length <= propVal;
      }
      return true; // if no value is found, it doesnt have a length. So thus it is less than the propVal
    },
    min: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length >= propVal;
      }
      return false; // opposite from above
    },
    date: function (propVal, eleVal) {
      if (eleVal){
        if (eleVal.length == 10) {
            if (new Date(eleVal) == "Invalid Date") {
                return false;
            }
            return true;
        }
        return false;
      }
      return false; // noting is an invalid date
    },
    birthdate: function (propVal, eleVal) {
        // check to see if it is a valid date
        if (!Kavie.validatorFunctions.date(propVal, eleVal)) {
            return false;
        }

        var date = new Date(eleVal);

        // check to see if the date is in the future
        if (date > new Date()) {
            return false;
        }

        // check to see if date is a rational birthdate
        var minDateAllowed = new Date();
        minDateAllowed.setFullYear(minDateAllowed.getFullYear() - 120); // 120 is age of oldest person allowd

        if (date < minDateAllowed) {
            return false;
        }

        return true;
    },
    regexPattern: function(propVal, eleVal){
      if (eleVal){
        return eleVal.toString().match(propVal) !== null;
      } else {
        return false;
      }
    }
  }

}(this.Kavie = this.Kavie || {}));

// This is the knockout js extender
// simply adds a few things to the observable so we can access these from the kavie object
ko.extenders.kavie = function (target, rules) {
    target.hasError = ko.observable(); // tracks whether this observable is valid or not

    // if contains addToArray, add this observable to the global Kavie object
    // (not sure if i like this)
    if (rules.addToArray){
      Kavie.add(target);
      delete rules.addToArray;
    }

    // if section exsists add observable to it
    if (rules.section){
      if (!Kavie.sections[rules.section]){
        Kavie.sections[rules.section] = [];
      }

      Kavie.sections[rules.section].push(target);

      delete rules.section;
    }

    // add the passed in rules to the observable
    target.rules = rules;

    // Simply checks each rule attached to this observable and changes hasError variable
    function validate(newValue) {
        var rules = target.rules;

        for (key in rules) {
            for (funcKey in Kavie.validatorFunctions) {
                if (key == funcKey) {
                    var isValid = Kavie.validatorFunctions[funcKey](rules[key], newValue);
                    if (!isValid) {
                        target.hasError(true);
                        return;
                    }
                }
            }
        }

        target.hasError(false);
    }

    target.startValidation = function () {
        target.subscription = target.subscribe(validate); // creates a subscribable to update when value changes
        validate(target());
    }

    target.stopValidation = function () {
        target.subscription.dispose();
        target.hasError(false);
    }

    return target;
};
<|endoftext|>


<commit_before>/*!
    Kavie - knockout observable validator
    Author: Matthew Nitschke
    License: MIT (http://www.opensource.org/licenses/mit-license.php)
    Version: 0.4.6
*/

// This is a singleton pattern for the Kavie object to validate against
;(function(ns) {

  // holds out observables and sections
  ns.sections = {};

  // turn validaton on
  ns.isValid = function(vm) {
    // vm can be a viewModel or a Kavie.section
    var isValid = true;

    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].startValidation();

      if (isValid) {
        if (kavieObservables[i].hasError()) {
          isValid = false;
        }
      }
    }

    return isValid;
  }

  ns.isSectionValid = function(sectionName) {
    var section = ns.sections[sectionName];

    var isValid = true;

    if (ko.unwrap(section.validate)) {

      var children = Object.keys(section.children);
      for (var i = 0; i < children.length; i++) {
        var childValid = ns.isSectionValid(children[i]); // recursivlly check children sections

        if (!childValid) { // if a child is not valid, the entire section isn't
          isValid = false;
        }
      }

      var sectionObsValid = ns.isValid(section.observables);

      if (!sectionObsValid){
        isValid = false;
      }

    } else {
      // if the section isn't validated, deactivate it
      ns.deactivateSection(sectionName);
    }

    return isValid;
  }

  // turns off validation
  ns.deactivate = function(vm) {
    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].stopValidation();
    }
  }

  ns.deactivateSection = function(sectionName) {
    var section = ns.sections[sectionName];

    var children = Object.keys(section.children);
    for(var i = 0; i < children.length; i ++){
      ns.deactivateSection(children[i]); // recursivlly go through all the section's children
    }

    ns.deactivate(section.observables);
  }

  ns.addVariableValidation = function(sectionName, shouldValidate) {
    var section = ns.sections[sectionName];
    if (!section){
      section = ns.sections[sectionName] = new KavieSection();
    }

    section.validate = shouldValidate;
  }

  ns.addSectionChild = function(parentSectionName, childSectionName) {
    var parentSection = ns.sections[parentSectionName];
    if (!parentSection) {
      parentSection = ns.sections[parentSectionName] = new KavieSection();
    }

    parentSection.children[childSectionName] = new KavieSection();
  }

  // simple helper method to see if an observable has been extended with the kavie extender
  var isKavieObservable = function(observable){
    return observable.hasOwnProperty("hasError"); // when you extend an observable with kavie, it addes hasError.
  }

  // returns an array of all kavieObservables found in the viewModel potentially passed in,
  // and attached to the Kavie object it's self
  var compileObservables = function(vm){

    var kavieObservables = [];

    if (vm && vm.hasOwnProperty("observables")){
      vm = vm.observables;
    }

    if (vm){
      var keys = Object.keys(vm);
      for(var i = 0; i < keys.length; i ++){
        if (isKavieObservable(vm[keys[i]])){
          kavieObservables.push(vm[keys[i]]);
        }
      }
    }

    return kavieObservables;
  }

  // built in validator functions
  ns.validatorFunctions = {
    required: function (propVal, eleVal) {
        if (propVal) {
            return !(eleVal == null || eleVal === '');
        } else {
            return true;
        }
    },
    numeric: function(propVal, eleVal){
      if (propVal){
         return !isNaN(parseFloat(eleVal)) && isFinite(eleVal);
      } else {
        return true;
      }

    },
    max: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length <= propVal;
      }
      return true; // if no value is found, it doesnt have a length. So thus it is less than the propVal
    },
    min: function (propVal, eleVal) {
      if (eleVal){
        return eleVal.length >= propVal;
      }
      return false; // opposite from above
    },
    matches: function(propVal, eleVal){
      if (ko.unwrap(propVal) == ko.unwrap(eleVal)){
        return true;
      }
      return false;
    },
    date: function (propVal, eleVal) {
      if (eleVal){
        if (eleVal.length == 10) {
            if (new Date(eleVal) == "Invalid Date") {
                return false;
            }
            return true;
        }
        return false;
      }
      return false; // noting is an invalid date
    },
    birthdate: function (propVal, eleVal) {
        // check to see if it is a valid date
        if (!Kavie.validatorFunctions.date(propVal, eleVal)) {
            return false;
        }

        var date = new Date(eleVal);

        // check to see if the date is in the future
        if (date > new Date()) {
            return false;
        }

        // check to see if date is a rational birthdate
        var minDateAllowed = new Date();
        minDateAllowed.setFullYear(minDateAllowed.getFullYear() - 120); // 120 is age of oldest person allowd

        if (date < minDateAllowed) {
            return false;
        }

        return true;
    },
    phone: function (propVal, eleVal) {
      if (eleVal){
        if (eleVal.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/)) {
            return true;
        } else {
            return false;
        }
      }
    },
    email: function (propVal, eleVal){
      if (eleVal){
        if (eleVal.match(/^(?:(?:[\w`~!#$%^&*\-=+;:{}'|,?\/]+(?:(?:\.(?:"(?:\\?[\w`~!#$%^&*\-=+;:{}'|,?\/\.()<>\[\] @]|\\"|\\\\)*"|[\w`~!#$%^&*\-=+;:{}'|,?\/]+))*\.[\w`~!#$%^&*\-=+;:{}'|,?\/]+)?)|(?:"(?:\\?[\w`~!#$%^&*\-=+;:{}'|,?\/\.()<>\[\] @]|\\"|\\\\)+"))@(?:[a-zA-Z\d\-]+(?:\.[a-zA-Z\d\-]+)*|\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\])$/)) {
            return true;
        } else {
            return false;
        }
      }
    },
    regexPattern: function(propVal, eleVal){
      if (eleVal){
        return eleVal.toString().match(propVal) !== null;
      } else {
        return false;
      }
    }
  }

}(this.Kavie = this.Kavie || {}));

function KavieSection(){
  var self = this;

  self.observables = [];

  self.children = {};

  self.validate = true; // initially always validate
}

// This is the knockout js extender
// simply adds a few things to the observable so we can access these from the kavie object
ko.extenders.kavie = function (target, rules) {
    // make a copy of rules because we delete from it, and that would delete the key from the object that is passed in
    var localRules = rules;

    target.hasError = ko.observable(); // tracks whether this observable is valid or not

    // if section exsists add observable to it
    if (localRules.section){
      if (!Kavie.sections[localRules.section]){
        Kavie.sections[localRules.section] = new KavieSection();
      }

      Kavie.sections[localRules.section].observables.push(target);
      localRules.section = "";
      //delete localRules.section;
    }

    // add the passed in rules to the observable
    target.rules = localRules;

    // Simply checks each rule attached to this observable and changes hasError variable
    function validate(newValue) {
        var rules = target.rules;

        for (key in rules) {
            for (funcKey in Kavie.validatorFunctions) {
                if (key == funcKey) {
                    var isValid = Kavie.validatorFunctions[funcKey](rules[key], newValue);
                    if (!isValid) {
                        target.hasError(true);
                        return;
                    }
                }
            }
        }

        target.hasError(false);
    }

    target.startValidation = function () {
        target.subscription = target.subscribe(validate); // creates a subscribable to update when value changes
        validate(target());
    }

    target.stopValidation = function () {
      if (target.subscription){
        target.subscription.dispose();
      }
      target.hasError(false);
    }

    return target;
};
<commit_msg>Added hasValue private method for validatorFunctions to use<commit_after>/*!
    Kavie - knockout observable validator
    Author: Matthew Nitschke
    License: MIT (http://www.opensource.org/licenses/mit-license.php)
    Version: 0.4.6
*/

// This is a singleton pattern for the Kavie object to validate against
;(function(ns){

  // holds out observables and sections
  ns.sections = {};

  // turn validaton on
  ns.isValid = function(vm){
    // vm can be a viewModel or a Kavie.section
    var isValid = true;

    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].startValidation();

      if (isValid) {
        if (kavieObservables[i].hasError()) {
          isValid = false;
        }
      }
    }

    return isValid;
  }

  ns.isSectionValid = function(sectionName){
    var section = ns.sections[sectionName];

    var isValid = true;

    if (ko.unwrap(section.validate)) {

      var children = Object.keys(section.children);
      for (var i = 0; i < children.length; i++) {
        var childValid = ns.isSectionValid(children[i]); // recursivlly check children sections

        if (!childValid) { // if a child is not valid, the entire section isn't
          isValid = false;
        }
      }

      var sectionObsValid = ns.isValid(section.observables);

      if (!sectionObsValid){
        isValid = false;
      }

    } else {
      // if the section isn't validated, deactivate it
      ns.deactivateSection(sectionName);
    }

    return isValid;
  }

  // turns off validation
  ns.deactivate = function(vm){
    var kavieObservables = compileObservables(vm);

    for(var i = 0; i < kavieObservables.length; i ++){
      kavieObservables[i].stopValidation();
    }
  }

  ns.deactivateSection = function(sectionName){
    var section = ns.sections[sectionName];

    var children = Object.keys(section.children);
    for(var i = 0; i < children.length; i ++){
      ns.deactivateSection(children[i]); // recursivlly go through all the section's children
    }

    ns.deactivate(section.observables);
  }

  ns.addVariableValidation = function(sectionName, shouldValidate){
    var section = ns.sections[sectionName];
    if (!section){
      section = ns.sections[sectionName] = new KavieSection();
    }

    section.validate = shouldValidate;
  }

  ns.addSectionChild = function(parentSectionName, childSectionName){
    var parentSection = ns.sections[parentSectionName];
    if (!parentSection) {
      parentSection = ns.sections[parentSectionName] = new KavieSection();
    }

    parentSection.children[childSectionName] = new KavieSection();
  }

  // simple helper method to see if an observable has been extended with the kavie extender
  var isKavieObservable = function(observable){
    return observable.hasOwnProperty("hasError"); // when you extend an observable with kavie, it addes hasError.
  }

  // returns an array of all kavieObservables found in the viewModel potentially passed in,
  // and attached to the Kavie object it's self
  var compileObservables = function(vm){

    var kavieObservables = [];

    if (vm && vm.hasOwnProperty("observables")){
      vm = vm.observables;
    }

    if (vm){
      var keys = Object.keys(vm);
      for(var i = 0; i < keys.length; i ++){
        if (isKavieObservable(vm[keys[i]])){
          kavieObservables.push(vm[keys[i]]);
        }
      }
    }

    return kavieObservables;
  }

  // built in validator functions
  // general rule for these is if empty, return true (if you want it required, you can add the required function)
  ns.validatorFunctions = {
    required: function (propVal, eleVal){
        if (propVal) {
            return hasValue(eleVal);
        } else {
            return true;
        }
    },
    numeric: function(propVal, eleVal){
      if (propVal){
         return !isNaN(parseFloat(eleVal)) && isFinite(eleVal);
      } else {
        return true;
      }

    },
    max: function (propVal, eleVal){
      if (eleVal){
        return eleVal.length <= propVal;
      }
      return true; // if no value is found, it doesnt have a length. So thus it is less than the propVal
    },
    min: function (propVal, eleVal){
      if (eleVal){
        return eleVal.length >= propVal;
      }
      return false; // opposite from above
    },
    matches: function(propVal, eleVal){
      if (ko.unwrap(propVal) == ko.unwrap(eleVal)){
        return true;
      }
      return false;
    },
    date: function (propVal, eleVal){
      if (eleVal){
        if (eleVal.length == 10) {
            if (new Date(eleVal) == "Invalid Date") {
                return false;
            }
            return true;
        }
        return false;
      }
      return false; // noting is an invalid date
    },
    birthdate: function (propVal, eleVal){
        // check to see if it is a valid date
        if (!Kavie.validatorFunctions.date(propVal, eleVal)) {
            return false;
        }

        var date = new Date(eleVal);

        // check to see if the date is in the future
        if (date > new Date()){
            return false;
        }

        // check to see if date is a rational birthdate
        var minDateAllowed = new Date();
        minDateAllowed.setFullYear(minDateAllowed.getFullYear() - 120); // 120 is age of oldest person allowd

        if (date < minDateAllowed){
            return false;
        }

        return true;
    },
    phone: function (propVal, eleVal){
      if (eleVal){
        if (eleVal.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/)) {
            return true;
        } else {
            return false;
        }
      }
    },
    email: function (propVal, eleVal){
      if (eleVal){
        if (eleVal.match(/^(?:(?:[\w`~!#$%^&*\-=+;:{}'|,?\/]+(?:(?:\.(?:"(?:\\?[\w`~!#$%^&*\-=+;:{}'|,?\/\.()<>\[\] @]|\\"|\\\\)*"|[\w`~!#$%^&*\-=+;:{}'|,?\/]+))*\.[\w`~!#$%^&*\-=+;:{}'|,?\/]+)?)|(?:"(?:\\?[\w`~!#$%^&*\-=+;:{}'|,?\/\.()<>\[\] @]|\\"|\\\\)+"))@(?:[a-zA-Z\d\-]+(?:\.[a-zA-Z\d\-]+)*|\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\])$/)) {
            return true;
        } else {
            return false;
        }
      }
    },
    regexPattern: function(propVal, eleVal){
      if (eleVal){
        return eleVal.toString().match(propVal) !== null;
      } else {
        return false;
      }
    }
  }

  var hasValue = function(value){
     return !(value == null || value.length === 0);
  }

}(this.Kavie = this.Kavie || {}));

function KavieSection(){
  var self = this;

  self.observables = [];

  self.children = {};

  self.validate = true; // initially always validate
}

// This is the knockout js extender
// simply adds a few things to the observable so we can access these from the kavie object
ko.extenders.kavie = function (target, rules){
    // make a copy of rules because we delete from it, and that would delete the key from the object that is passed in
    var localRules = rules;

    target.hasError = ko.observable(); // tracks whether this observable is valid or not

    // if section exsists add observable to it
    if (localRules.section){
      if (!Kavie.sections[localRules.section]){
        Kavie.sections[localRules.section] = new KavieSection();
      }

      Kavie.sections[localRules.section].observables.push(target);
      localRules.section = "";
      //delete localRules.section;
    }

    // add the passed in rules to the observable
    target.rules = localRules;

    // Simply checks each rule attached to this observable and changes hasError variable
    function validate(newValue){
        var rules = target.rules;

        for (key in rules){
            for (funcKey in Kavie.validatorFunctions){
                if (key == funcKey) {
                    var isValid = Kavie.validatorFunctions[funcKey](rules[key], newValue);
                    if (!isValid) {
                        target.hasError(true);
                        return;
                    }
                }
            }
        }

        target.hasError(false);
    }

    target.startValidation = function(){
        target.subscription = target.subscribe(validate); // creates a subscribable to update when value changes
        validate(target());
    }

    target.stopValidation = function(){
      if (target.subscription){
        target.subscription.dispose();
      }
      target.hasError(false);
    }

    return target;
};
<|endoftext|>


<commit_before>$(document).ready(function() {
  var btnreset = $("#btnreset");
  var btnsend = $("#btnsend");
  var btnnew = $("#btnnew");
  var btnsyntax = $("#btnsyntaxhighlight");
  var txtcode = $("#txtcode");
  var statusmessagediv = $("#statusmessage");
  var statusdiv = $("#status");
  var showpastediv = $("#showpaste");
  var codepaste = $("#codepaste");
  var newpastediv = $("#newpaste");

  btnreset.on('click', function() {
    txtcode.val("");
    btnsend.prop( "disabled", false );
    statusdiv.css("display","none");
  });

  btnnew.on('click', function() {
    showpastediv.hide();
    newpastediv.show();
  });

  btnsyntax.on('click', function() {
    $('#codepaste').each(function(i, block) {
      hljs.highlightBlock(block);
    });
  });

  btnsend.on('click', function() {
    var plaintext = txtcode.val();

    if(plaintext == "") {
      showStatus('No code to paste :(','danger');
      return ;
    }

    // generate a random key and IV for AES-256
    var key = forge.random.getBytesSync(32);
    var iv = forge.random.getBytesSync(32);

    //Encrypt data with AES-256-CBC
    encrypted = AESEncrypt(plaintext, key, iv);

    if(encrypted == null) {
      showStatus('An error occurred during encryption', 'danger');
      return ;
    }

    btnsend.prop( "disabled", true );
    //Send data to server
    $.ajax({
      type: "POST",
      url: "/api/createpaste",
      data: {
        data: encrypted
      }
    })
      .done(function( msg ) {

        if(msg.error != null) {
          showStatus(msg.error, 'danger');
          return ;
        }

        if(typeof msg.hash === undefined ) {
          showStatus('Failed to get paste hash', 'danger');
          return ;
        }

        var protocol = location.protocol
        var host = location.host
        var path = location.pathname

        var b64data = forge.util.encode64(key + "||" + iv + "||" + msg.hash)
        var url = protocol + host + path + "#" + b64data

        statusmessagediv.html("Paste saved! <input class='form-control' type='text' id='pasteurl' value='"+url+"'>");
        statusmessagediv.addClass("alert-success");
        statusdiv.fadeIn().css("display","block").delay(10000).fadeOut("fast", function() {
          statusmessagediv.toggleClass("alert-success");
        });

        $("#pasteurl").select();
        btnsend.prop( "disabled", false );
    })
      .fail(function( msg ) {
        showStatus('Unable to save the paste :(', 'danger');
        return ;
    });
  });

  //Do we need to decrypt a paste?
  if(location.hash.slice(1) !== "")
  {
    newpastediv.hide();
    codepaste.text('Loading...');
    showpastediv.fadeIn("fast");
    //Get the data from the location-hash string
    var data = location.hash.slice(1);
    try {
      var decoded = forge.util.decode64(data);
      decoded = decoded.split("||");
      var key = decoded[0] || null
      var iv = decoded[1] || null
      var hash = decoded[2] || null
    } catch(e) {
      showStatus('Failed to decrypt the paste','danger');
      return ;
    }

    $.ajax({
      type: "POST",
      url: "/api/retrievepaste",
      data: {
        hash: hash
      }
    })
      .done(function(msg) {
        if(msg.error != null) {
          showStatus(msg.error, 'danger');
          return ;
        }

        if(typeof msg.data === undefined ) {
          showStatus('Failed to retrieve the paste from the server :(', 'danger');
          return ;
        }

        decrypted = AESDecrypt(msg.data, key, iv);

        if(decrypted == null) {
          showStatus('An error occurred during the decryption process', 'danger');
          return ;
        }

        codepaste.text(decrypted);
    })
      .fail(function(msg) {
        showStatus('Failed to retrieve the paste from the server :(','danger');
        return;
    })
  }

});

/**
  * Encrypts data with AES-CBC. Strength depends on the key, iv size.
  * @param: plaintext - Text to encrypt
  * @param: key - key in byteform
  * @param: iv - key in byteform
**/
function AESEncrypt(plaintext, key, iv) {
  try {
    var cipher = forge.cipher.createCipher('AES-CBC', key);
    cipher.start({iv: iv});
    cipher.update(forge.util.createBuffer(plaintext));
    cipher.finish();
    return cipher.output.toHex();
  } catch(e) {
    return null;
  }
}

/**
  * Decrypts data with AES-CBC.
  * @param: encrypted - Text to decrypt
  * @param: key - key in byteform
  * @param: iv - key in byteform
**/
function AESDecrypt(encrypted, key, iv) {
  try {
    var decipher = forge.cipher.createDecipher('AES-CBC', key);
    decipher.start({iv: iv});
    decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encrypted)));
    decipher.finish();
    return decipher.output.toString();
  } catch(e) {
    return null;
  }
}

/**
  * Displays a message in the #statusmessage div and fadesIn/fadesOut the #status div.
  * @param: text - Text to display
  * @param: type - Boostrap css alert-[type] style. Do not pass user-input here. Other XSS may be possible!
  * @param: duration - How long the message is displayed
**/
function showStatus(text,type) {
  var duration = duration || 5000;
  var statusmessagediv = $("#statusmessage");
  var statusdiv = $("#status");
  statusmessagediv.text(text);
  statusmessagediv.addClass("alert-"+type);
  statusdiv.fadeIn().css("display","block").delay(duration).fadeOut("fast", function() {
    statusmessagediv.toggleClass("alert-"+type);
  });
}<commit_msg>Some more poorly written comments... ;)<commit_after>$(document).ready(function() {
  var btnreset = $("#btnreset");
  var btnsend = $("#btnsend");
  var btnnew = $("#btnnew");
  var btnsyntax = $("#btnsyntaxhighlight");
  var txtcode = $("#txtcode");
  var statusmessagediv = $("#statusmessage");
  var statusdiv = $("#status");
  var showpastediv = $("#showpaste");
  var codepaste = $("#codepaste");
  var newpastediv = $("#newpaste");

  btnreset.on('click', function() {
    txtcode.val("");
    btnsend.prop( "disabled", false );
    statusdiv.css("display","none");
  });

  btnnew.on('click', function() {
    showpastediv.hide();
    newpastediv.show();
  });

  btnsyntax.on('click', function() {
    $('#codepaste').each(function(i, block) {
      hljs.highlightBlock(block);
    });
  });

  btnsend.on('click', function() {
    var plaintext = txtcode.val();

    if(plaintext == "") {
      showStatus('No code to paste :(','danger');
      return ;
    }

    // generate a random key and IV for AES-256
    var key = forge.random.getBytesSync(32);
    var iv = forge.random.getBytesSync(32);

    //Encrypt data with AES-256-CBC
    encrypted = AESEncrypt(plaintext, key, iv);

    if(encrypted == null) {
      showStatus('An error occurred during encryption', 'danger');
      return ;
    }

    btnsend.prop( "disabled", true );
    //Send data to server
    $.ajax({
      type: "POST",
      url: "/api/createpaste",
      data: {
        data: encrypted
      }
    })
      .done(function( msg ) {

        if(msg.error != null) {
          showStatus(msg.error, 'danger');
          return ;
        }

        if(typeof msg.hash === undefined ) {
          showStatus('Failed to get paste hash', 'danger');
          return ;
        }

        //Build URL with paste information in the hashtag
        var protocol = location.protocol
        var host = location.host
        var path = location.pathname

        var b64data = forge.util.encode64(key + "||" + iv + "||" + msg.hash)
        var url = protocol + host + path + "#" + b64data

        //Display the link for 10 seconds
        statusmessagediv.html("Paste saved! <input class='form-control' type='text' id='pasteurl' value='"+url+"'>");
        statusmessagediv.addClass("alert-success");
        statusdiv.fadeIn().css("display","block").delay(10000).fadeOut("fast", function() {
          statusmessagediv.toggleClass("alert-success");
        });

        $("#pasteurl").select();
        btnsend.prop( "disabled", false );
    })
      .fail(function( msg ) {
        showStatus('Unable to save the paste :(', 'danger');
        return ;
    });
  });

  //Do we need to decrypt a paste?
  if(location.hash.slice(1) !== "")
  {
    newpastediv.hide();
    codepaste.text('Loading...');
    showpastediv.fadeIn("fast");

    //Get the data from the location-hash string
    var data = location.hash.slice(1);
    try {
      var decoded = forge.util.decode64(data);
      decoded = decoded.split("||");
      var key = decoded[0] || null
      var iv = decoded[1] || null
      var hash = decoded[2] || null
    } catch(e) {
      showStatus('Failed to decrypt the paste','danger');
      return ;
    }

    //Retrieve the encrypted paste data from the server
    $.ajax({
      type: "POST",
      url: "/api/retrievepaste",
      data: {
        hash: hash
      }
    })
      .done(function(msg) {
        if(msg.error != null) {
          showStatus(msg.error, 'danger');
          return ;
        }

        if(typeof msg.data === undefined ) {
          showStatus('Failed to retrieve the paste from the server :(', 'danger');
          return ;
        }

        decrypted = AESDecrypt(msg.data, key, iv);

        if(decrypted == null) {
          showStatus('An error occurred during the decryption process', 'danger');
          return ;
        }

        codepaste.text(decrypted);
    })
      .fail(function(msg) {
        showStatus('Failed to retrieve the paste from the server :(','danger');
        return;
    })
  }

});

/**
  * Encrypts data with AES-CBC. Strength depends on the key, iv size.
  * @param: plaintext - Text to encrypt
  * @param: key - key in byteform
  * @param: iv - key in byteform
**/
function AESEncrypt(plaintext, key, iv) {
  try {
    var cipher = forge.cipher.createCipher('AES-CBC', key);
    cipher.start({iv: iv});
    cipher.update(forge.util.createBuffer(plaintext));
    cipher.finish();
    return cipher.output.toHex();
  } catch(e) {
    return null;
  }
}

/**
  * Decrypts data with AES-CBC.
  * @param: encrypted - Text to decrypt
  * @param: key - key in byteform
  * @param: iv - key in byteform
**/
function AESDecrypt(encrypted, key, iv) {
  try {
    var decipher = forge.cipher.createDecipher('AES-CBC', key);
    decipher.start({iv: iv});
    decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encrypted)));
    decipher.finish();
    return decipher.output.toString();
  } catch(e) {
    return null;
  }
}

/**
  * Displays a message in the #statusmessage div and fadesIn/fadesOut the #status div.
  * @param: text - Text to display
  * @param: type - Boostrap css alert-[type] style. Do not pass user-input here. Other XSS may be possible!
  * @param: duration - How long the message is displayed
**/
function showStatus(text,type) {
  var duration = duration || 5000;
  var statusmessagediv = $("#statusmessage");
  var statusdiv = $("#status");
  statusmessagediv.text(text);
  statusmessagediv.addClass("alert-"+type);
  statusdiv.fadeIn().css("display","block").delay(duration).fadeOut("fast", function() {
    statusmessagediv.toggleClass("alert-"+type);
  });
}<|endoftext|>