<commit_before>/**
 * This file contains logic for popular posts. We keep 3 days' top-100
 * popular posts per category in Redis. We also need to use a background
 * schedule task to save top posts (max top 10 per day per category) into
 * popular_post_bucket Cassandra table.
 */

var redis = require("redis");
var client = redis.createClient();
var moment = require("moment");
var util = require('util');
var Q = require('q');
var cql = require('node-cassandra-cql'),
  dbClient = new cql.Client({hosts: ['localhost'], keyspace: 'myks'});
var dbPromise = require('./db/promise');
var categories = require('./categories');

// if you'd like to select database 3, instead of 0 (default), call
// client.select(3, function() { /* ... */ });
client.on("error", function (err) {
  console.error("Redis error " + err);
});

//redis key for sorted set for top posts of a category, top_posts:3m
// The value in the sorted set has the format of
// <timestamp in seconds>|<post id>, in which timestamp is timestamp info
// of the post id (timeUUID)
var TOP_POSTS = "top_posts:%s";

// Max posts per category
var MAX_POSTS_PER_CAT = 100;

// Save 10 top posts per day per category into Cassandra db.
var COUNT_SAVE_PER_DAY_PER_CAT = 10;

// The map[category, score] maintains the min score of popular posts in
// each category.
var minScoreTopPosts = {};

// Top posts should expired in "top_posts:%s" key after 3 days.
var topPostExpirationInSecond = 3 * 24 * 3600;

// The timestamp in seconds of last time we clean up expired top post in Redis
// "top_posts:%s" key. Since Redis doesn't has expiration for set members,
// we have to expire them by ourselves.
var lastTimeCleanUpExpiredTopPosts = 0;

// The interval to clean up expired top posts
var cleanExpiredIntervalInSeconds = 6 * 3600;

function genPostValueForCache(timestampInSeconds, postId) {
  return timestampInSeconds + "|" + postId;
}

/**
 * Clean up expired (more than 3 days old) from top post Redis key.
 * @param key top post key "top_posts:%s", e.g. "top_posts:3m"
 */
function cleanUpExpiredTopPosts(key) {
  console.log("In cleanUpExpiredTopPosts");
  client.zrange(key, 0, -1, function (err, values) {
    if (err) {
      console.error("Error in cleanUpExpiredTopPosts, " + err);
    } else {
      var currentTimeInSeconds = Math.floor(moment() / 1000);
      var valuesToClear = [key];
      values.forEach(function (value) {
        var timestamp = value.split("|")[0];
        if (timestamp &&
            currentTimeInSeconds - timestamp > topPostExpirationInSecond) {
          // This value is stale
          valuesToClear.push(value);
        }
      });

      if (valuesToClear.length > 1) {
        console.log("ZREM, valuesToClear=" + valuesToClear);
        client.zrem(valuesToClear,
            function (err, response) {
              if (err) {
                console.error("Error in cleanUpExpiredTopPosts, " + err);
              }
        });
      }
    }
  });
}

/**
 * Update category popular posts if the post is popular enough
 * @param category string, the category of the post, e.g. 3m, 1y
 * @param likes int, count of likes on the post
 * @param comments int, count of comments on the post
 * @param timestampInSeconds int, timestamp in seconds (time info in postId)
 * @param postId string post id
 * @param cb callback
 */
function maybeUpdatePopularPost(category, likes, comments, timestampInSeconds, postId, cb) {
  var postScore = comments * 2 + likes;
  var minScore = minScoreTopPosts[category];
  var key = util.format(TOP_POSTS, category);

  var interval = Math.floor(moment() / 1000) - lastTimeCleanUpExpiredTopPosts;

  if (interval > cleanExpiredIntervalInSeconds) {
    console.log("lastTimeCleanUpExpiredTopPosts=" +
        lastTimeCleanUpExpiredTopPosts +
        ", interval=" + interval);
    cleanUpExpiredTopPosts(key);
  }

  console.log("minScore=" + minScore + ", postScore=" + postScore);

  if (minScore && minScore >= postScore) {
    // The post is not popular enough to go to top posts cache.
    return;
  }

  Q.ninvoke(client, 'ZADD', key, postScore,
      genPostValueForCache(timestampInSeconds, postId))
  .then(function(result) {
    console.log("Result of ZADD, " + result);
    if (result > 0) {
      // A new popular post is inserted, then we get the count of
      // posts for this key
      return Q.ninvoke(client, 'ZCARD', key);
    } else {
      // It is the case that an existing popular post gets a new score.
      return 0;
    }
  })
  .then(function(count) {
    console.log("Result of ZCARD, " + count);
    if (count > MAX_POSTS_PER_CAT) {
      // Keep max MAX_POSTS_PER_CAT posts.
      return Q.ninvoke(client, 'ZREMRANGEBYRANK', key, 0,
          count - MAX_POSTS_PER_CAT - 1);
    }
  })
  .then(function (result) {
    console.log("Result of ZREMRANGEBYRANK, " + result);
    // Update the min score
    return Q.ninvoke(client, 'ZRANGE', key, 0, 0, 'WITHSCORES');
  })
  .then(function (minPost) {
    console.log("minPost=" + minPost);
    if (minPost) {
      console.log("category=" + category + ", minPost[1]=" + minPost[1]);
      minScoreTopPosts[category] = minPost[1];
    }
  })
  .then(
    function (result) {
      console.log("maybeUpdatePopularPost succeeded");
      cb();
    },
    function (error) {
      console.error("Redis error for maybeUpdatePopularPost" + error);
  });
}

/**
 * Save top posts of a category key to Cassandra db.
 * It saves top posts from 48 hours ago to 24 hours ago into Cassandra db.
 * Each time, it only saves the top 10 posts for each category.
 * @param key Redis key, e.g. 'top_posts:3m'
 */
function saveTopPostsToDbByKey(key) {
	console.log("In saveTopPostsToDbByKey for " + key);
	var nowInSeconds = Math.floor(moment());
	var startTimeInSeconds = nowInSeconds - 48 * 3600;
	var endTimeInSeconds = nowInSeconds - 24 * 3600;

	var valuesToSave = [];
  Q.ninvoke(client, 'ZREVRANGE', key, 0, -1)
    .then(function (values) {
      console.log("Results of ZREVRANGE, key=" + key + ', values=' + values);
      values.forEach(function (value) {
        if (valuesToSave.length === COUNT_SAVE_PER_DAY_PER_CAT) {
          return valuesToSave;
        }
        var timestamp = value.split("|")[0];
        var postId = value.split("|")[1];
        if (postId &&
            timestamp >= startTimeInSeconds &&
            timestamp <= endTimeInSeconds) {
          valuesToSave.push({timestamp: timestamp, postId: postId});
        }
      });
      return valuesToSave;
	})
	.then(function (valuesToSave) {
      if (valuesToSave.length === 0) {
        return;
      }
      var category = key.split(":")[1];
      var batchParams = [];
      valuesToSave.forEach(function (value) {
        var timestampInMs = value.timestamp * 1000;
        var yymm = moment(timestampInMs).format('YYMM');
        batchParams.push([category + '|' + yymm,
          {value: value.postId, hint: cql.types.dataTypes.timeuuid}]);
      });
      return dbPromise.genBatchQueryPromise(
        'INSERT INTO popular_post_bucket ' +
          '(post_category_date, post_id) ' +
        'VALUES (?, ?)',
        batchParams);
    })
	.then(
      function (result) {
        console.log("saveTopPostsToDbByKey succeeded");
      },
      function (error) {
        console.trace("Error for saveTopPostsToDbByKey " + error);
    });
}

/**
 * Save top posts to Cassandra db. This function should be called by a
 * background task every day. Each time, it saves top posts from 48 hours
 * ago to 24 hours ago into Cassandra db. Each time, it only saves
 * the top 10 posts for each category.
 */
function saveTopPostsToDb() {
	console.log("In saveTopPostsToDb");

	categories.getPostCategires().forEach(function(category) {
    saveTopPostsToDbByKey(util.format(TOP_POSTS, category));
	});
}

//saveTopPostsToDb();
//maybeUpdatePopularPost("1m", 6, 2, 1408123996, cql.types.timeuuid());
exports.maybeUpdatePopularPost = maybeUpdatePopularPost;
// TODO (kpan): add unit tests .<commit_msg>To be consistent, use : as separater instead of |.<commit_after>/**
 * This file contains logic for popular posts. We keep 3 days' top-100
 * popular posts per category in Redis. We also need to use a background
 * schedule task to save top posts (max top 10 per day per category) into
 * popular_post_bucket Cassandra table.
 */

var redis = require("redis");
var client = redis.createClient();
var moment = require("moment");
var util = require('util');
var Q = require('q');
var cql = require('node-cassandra-cql'),
  dbClient = new cql.Client({hosts: ['localhost'], keyspace: 'myks'});
var dbPromise = require('./db/promise');
var categories = require('./categories');

// if you'd like to select database 3, instead of 0 (default), call
// client.select(3, function() { /* ... */ });
client.on("error", function (err) {
  console.error("Redis error " + err);
});

//redis key for sorted set for top posts of a category, top_posts:3m
// The value in the sorted set has the format of
// <timestamp in seconds>:<post id>, in which timestamp is timestamp info
// of the post id (timeUUID)
var TOP_POSTS = "top_posts:%s";

// Max posts per category
var MAX_POSTS_PER_CAT = 100;

// Save 10 top posts per day per category into Cassandra db.
var COUNT_SAVE_PER_DAY_PER_CAT = 10;

// The map[category, score] maintains the min score of popular posts in
// each category.
var minScoreTopPosts = {};

// Top posts should expired in "top_posts:%s" key after 3 days.
var topPostExpirationInSecond = 3 * 24 * 3600;

// The timestamp in seconds of last time we clean up expired top post in Redis
// "top_posts:%s" key. Since Redis doesn't has expiration for set members,
// we have to expire them by ourselves.
var lastTimeCleanUpExpiredTopPosts = 0;

// The interval to clean up expired top posts
var cleanExpiredIntervalInSeconds = 6 * 3600;

function genPostValueForCache(timestampInSeconds, postId) {
  return timestampInSeconds + ":" + postId;
}

/**
 * Clean up expired (more than 3 days old) from top post Redis key.
 * @param key top post key "top_posts:%s", e.g. "top_posts:3m"
 */
function cleanUpExpiredTopPosts(key) {
  console.log("In cleanUpExpiredTopPosts");
  client.zrange(key, 0, -1, function (err, values) {
    if (err) {
      console.error("Error in cleanUpExpiredTopPosts, " + err);
    } else {
      var currentTimeInSeconds = Math.floor(moment() / 1000);
      var valuesToClear = [key];
      values.forEach(function (value) {
        var timestamp = value.split(":")[0];
        if (timestamp &&
            currentTimeInSeconds - timestamp > topPostExpirationInSecond) {
          // This value is stale
          valuesToClear.push(value);
        }
      });

      if (valuesToClear.length > 1) {
        console.log("ZREM, valuesToClear=" + valuesToClear);
        client.zrem(valuesToClear,
            function (err, response) {
              if (err) {
                console.error("Error in cleanUpExpiredTopPosts, " + err);
              }
        });
      }
    }
  });
}

/**
 * Update category popular posts if the post is popular enough
 * @param category string, the category of the post, e.g. 3m, 1y
 * @param likes int, count of likes on the post
 * @param comments int, count of comments on the post
 * @param timestampInSeconds int, timestamp in seconds (time info in postId)
 * @param postId string post id
 * @param cb callback
 */
function maybeUpdatePopularPost(category, likes, comments, timestampInSeconds, postId, cb) {
  var postScore = comments * 2 + likes;
  var minScore = minScoreTopPosts[category];
  var key = util.format(TOP_POSTS, category);

  var interval = Math.floor(moment() / 1000) - lastTimeCleanUpExpiredTopPosts;

  if (interval > cleanExpiredIntervalInSeconds) {
    console.log("lastTimeCleanUpExpiredTopPosts=" +
        lastTimeCleanUpExpiredTopPosts +
        ", interval=" + interval);
    cleanUpExpiredTopPosts(key);
  }

  console.log("minScore=" + minScore + ", postScore=" + postScore);

  if (minScore && minScore >= postScore) {
    // The post is not popular enough to go to top posts cache.
    return;
  }

  Q.ninvoke(client, 'ZADD', key, postScore,
      genPostValueForCache(timestampInSeconds, postId))
  .then(function(result) {
    console.log("Result of ZADD, " + result);
    if (result > 0) {
      // A new popular post is inserted, then we get the count of
      // posts for this key
      return Q.ninvoke(client, 'ZCARD', key);
    } else {
      // It is the case that an existing popular post gets a new score.
      return 0;
    }
  })
  .then(function(count) {
    console.log("Result of ZCARD, " + count);
    if (count > MAX_POSTS_PER_CAT) {
      // Keep max MAX_POSTS_PER_CAT posts.
      return Q.ninvoke(client, 'ZREMRANGEBYRANK', key, 0,
          count - MAX_POSTS_PER_CAT - 1);
    }
  })
  .then(function (result) {
    console.log("Result of ZREMRANGEBYRANK, " + result);
    // Update the min score
    return Q.ninvoke(client, 'ZRANGE', key, 0, 0, 'WITHSCORES');
  })
  .then(function (minPost) {
    console.log("minPost=" + minPost);
    if (minPost) {
      console.log("category=" + category + ", minPost[1]=" + minPost[1]);
      minScoreTopPosts[category] = minPost[1];
    }
  })
  .then(
    function (result) {
      console.log("maybeUpdatePopularPost succeeded");
      cb();
    },
    function (error) {
      console.error("Redis error for maybeUpdatePopularPost" + error);
  });
}

/**
 * Save top posts of a category key to Cassandra db.
 * It saves top posts from 48 hours ago to 24 hours ago into Cassandra db.
 * Each time, it only saves the top 10 posts for each category.
 * @param key Redis key, e.g. 'top_posts:3m'
 */
function saveTopPostsToDbByKey(key) {
	console.log("In saveTopPostsToDbByKey for " + key);
	var nowInSeconds = Math.floor(moment());
	var startTimeInSeconds = nowInSeconds - 48 * 3600;
	var endTimeInSeconds = nowInSeconds - 24 * 3600;

	var valuesToSave = [];
  Q.ninvoke(client, 'ZREVRANGE', key, 0, -1)
    .then(function (values) {
      console.log("Results of ZREVRANGE, key=" + key + ', values=' + values);
      values.forEach(function (value) {
        if (valuesToSave.length === COUNT_SAVE_PER_DAY_PER_CAT) {
          return valuesToSave;
        }
        var timestamp = value.split(":")[0];
        var postId = value.split(":")[1];
        if (postId &&
            timestamp >= startTimeInSeconds &&
            timestamp <= endTimeInSeconds) {
          valuesToSave.push({timestamp: timestamp, postId: postId});
        }
      });
      return valuesToSave;
	})
	.then(function (valuesToSave) {
      if (valuesToSave.length === 0) {
        return;
      }
      var category = key.split(":")[1];
      var batchParams = [];
      valuesToSave.forEach(function (value) {
        var timestampInMs = value.timestamp * 1000;
        var yymm = moment(timestampInMs).format('YYMM');
        batchParams.push([category + ':' + yymm,
          {value: value.postId, hint: cql.types.dataTypes.timeuuid}]);
      });
      return dbPromise.genBatchQueryPromise(
        'INSERT INTO popular_post_bucket ' +
          '(post_category_date, post_id) ' +
        'VALUES (?, ?)',
        batchParams);
    })
	.then(
      function (result) {
        console.log("saveTopPostsToDbByKey succeeded");
      },
      function (error) {
        console.trace("Error for saveTopPostsToDbByKey " + error);
    });
}

/**
 * Save top posts to Cassandra db. This function should be called by a
 * background task every day. Each time, it saves top posts from 48 hours
 * ago to 24 hours ago into Cassandra db. Each time, it only saves
 * the top 10 posts for each category.
 */
function saveTopPostsToDb() {
	console.log("In saveTopPostsToDb");

	categories.getPostCategires().forEach(function(category) {
    saveTopPostsToDbByKey(util.format(TOP_POSTS, category));
	});
}

//saveTopPostsToDb();
//maybeUpdatePopularPost("1m", 6, 2, 1408123996, cql.types.timeuuid());
exports.maybeUpdatePopularPost = maybeUpdatePopularPost;
// TODO (kpan): add unit tests .<|endoftext|>


<commit_before>'use strict';

/** Load Node.js modules. */
var fs = require('fs');

/** Load other modules. */
var _ = require('lodash');

/** The minimal license/copyright template. */
var licenseTemplate = [
  '/**',
  ' * @license',
  ' * lodash <%= lodash.VERSION %> lodash.com/license | Underscore.js <%= underscore.VERSION %> underscorejs.org/LICENSE',
  ' */'
].join('\n');

/*----------------------------------------------------------------------------*/

/**
 * Post-process a given minified lodash `source`, preparing it for
 * deployment.
 *
 * @param {string} source The source to process.
 * @param {boolean} [isMapped] Specify whether `source` has a source map.
 * @returns {string} Returns the processed source.
 */
function postprocess(source, isMapped) {
  // Add trailing semicolon.
  source = source.replace(/[\s;]*(\n\s*\/\/.*)?\s*$/, ';$1');

  // Exit early if `source` has a source map.
  if (isMapped) {
    return source;
  }
  // Remove copyright header.
  var header = _.result(/^\/\**[\s\S]+?\*\/\n/.exec(source), 0, '');
  source = source.replace(header, '');

  // Add new copyright header.
  source = _.template(licenseTemplate)({
    'lodash': _,
    'underscore': { 'VERSION': _.result(/\bUnderscore\.js ([.$\w\-]+)/i.exec(header), 1, '') }
  }) + '\n;' + source;

  // Replace `!0` and `!1` with `true` and `false`.
  source = source.replace(/(.)(\![01])\b/g, function(match, chr, exp) {
    return chr + (/[$\w]/.test(chr) ? ' ' : '') + (exp == '!0');
  });

  // Flip `typeof` expressions to help optimize Safari and
  // correct the AMD module definition for AMD build optimizers
  // (e.g. from `"number" == typeof x` to `typeof x == "number").
  source = source.replace(/([$\w])?("[^"]+")\s*([!=]=)\s*(typeof(?:\s*\([^)]+\)|\s+[$\w]+(?:\s*\.\s*[$\w]+)*([[(])?))/g, function(match, left, type, equality, exp, right) {
    return right ? match : (left ? left + ' ' : '') + exp + equality + type;
  });

  // Add a space so `define` is detected by the Dojo builder.
  source = source.replace(/(.)(define\()/, function(match, prelude, define) {
    return prelude + (/^\S/.test(prelude) ? ' ' : '') +  define;
  });

  // Correct overly aggressive Closure Compiler advanced optimizations.
  source = source
    .replace(/"\t"/g, '"\\t"')
    .replace(/"[^"]*?\\f[^"]*?"/g,
      '" \\t\\x0b\\f\\xa0\\ufeff' +
      '\\n\\r\\u2028\\u2029' +
      '\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000"'
    );

  // Consolidate multiple newlines.
  return source.replace(/\n{2,}/g, '\n');
}

/*----------------------------------------------------------------------------*/

// Export `postprocess`.
if (module != require.main) {
  module.exports = postprocess;
}
// Read the lodash source file from the first argument if the script was invoked
// by the command-line (e.g. `node post-compile.js source.js`) and write to the same file.
else if (_.size(process.argv) > 2) {
  (function() {
    var filePath = path.normalize(_.last(process.argv)),
        source = fs.readFileSync(filePath, 'utf8');

    fs.writeFileSync(filePath, postprocess(source), 'utf8');
  }());
}
<commit_msg>Rename `licenseTemplate` to `headerTemplate`.<commit_after>'use strict';

/** Load Node.js modules. */
var fs = require('fs');

/** Load other modules. */
var _ = require('lodash');

/** The minimal copyright header template. */
var headerTemplate = [
  '/**',
  ' * @license',
  ' * lodash <%= lodash.VERSION %> lodash.com/license | Underscore.js <%= underscore.VERSION %> underscorejs.org/LICENSE',
  ' */'
].join('\n');

/*----------------------------------------------------------------------------*/

/**
 * Post-process a given minified lodash `source`, preparing it for
 * deployment.
 *
 * @param {string} source The source to process.
 * @param {boolean} [isMapped] Specify whether `source` has a source map.
 * @returns {string} Returns the processed source.
 */
function postprocess(source, isMapped) {
  // Add trailing semicolon.
  source = source.replace(/[\s;]*(\n\s*\/\/.*)?\s*$/, ';$1');

  // Exit early if `source` has a source map.
  if (isMapped) {
    return source;
  }
  // Remove copyright header.
  var header = _.result(/^\/\**[\s\S]+?\*\/\n/.exec(source), 0, '');
  source = source.replace(header, '');

  // Add new copyright header.
  source = _.template(headerTemplate)({
    'lodash': _,
    'underscore': { 'VERSION': _.result(/\bUnderscore\.js ([.$\w\-]+)/i.exec(header), 1, '') }
  }) + '\n;' + source;

  // Replace `!0` and `!1` with `true` and `false`.
  source = source.replace(/(.)(\![01])\b/g, function(match, chr, exp) {
    return chr + (/[$\w]/.test(chr) ? ' ' : '') + (exp == '!0');
  });

  // Flip `typeof` expressions to help optimize Safari and
  // correct the AMD module definition for AMD build optimizers
  // (e.g. from `"number" == typeof x` to `typeof x == "number").
  source = source.replace(/([$\w])?("[^"]+")\s*([!=]=)\s*(typeof(?:\s*\([^)]+\)|\s+[$\w]+(?:\s*\.\s*[$\w]+)*([[(])?))/g, function(match, left, type, equality, exp, right) {
    return right ? match : (left ? left + ' ' : '') + exp + equality + type;
  });

  // Add a space so `define` is detected by the Dojo builder.
  source = source.replace(/(.)(define\()/, function(match, prelude, define) {
    return prelude + (/^\S/.test(prelude) ? ' ' : '') +  define;
  });

  // Correct overly aggressive Closure Compiler advanced optimizations.
  source = source
    .replace(/"\t"/g, '"\\t"')
    .replace(/"[^"]*?\\f[^"]*?"/g,
      '" \\t\\x0b\\f\\xa0\\ufeff' +
      '\\n\\r\\u2028\\u2029' +
      '\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000"'
    );

  // Consolidate multiple newlines.
  return source.replace(/\n{2,}/g, '\n');
}

/*----------------------------------------------------------------------------*/

// Export `postprocess`.
if (module != require.main) {
  module.exports = postprocess;
}
// Read the lodash source file from the first argument if the script was invoked
// by the command-line (e.g. `node post-compile.js source.js`) and write to the same file.
else if (_.size(process.argv) > 2) {
  (function() {
    var filePath = path.normalize(_.last(process.argv)),
        source = fs.readFileSync(filePath, 'utf8');

    fs.writeFileSync(filePath, postprocess(source), 'utf8');
  }());
}
<|endoftext|>


<commit_before>/**
 * power-assert - Empower your assertions
 *
 * https://github.com/twada/power-assert
 *
 * Copyright (c) 2013 Takuto Wada
 * Licensed under the MIT license.
 *   https://raw.github.com/twada/power-assert/master/MIT-LICENSE.txt
 */
(function (root, factory) {
    'use strict';

    // using returnExports UMD pattern
    if (typeof define === 'function' && define.amd) {
        define(['assert', 'empower'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('assert'), require('empower'));
    } else {
        root.assert = factory(root.assert, root.empower);
    }
}(this, function (baseAssert, empower) {
    'use strict';

    return empower(baseAssert);
}));
<commit_msg>Sync header-line with README.<commit_after>/**
 * power-assert.js - Power Assert in JavaScript.
 *
 * https://github.com/twada/power-assert
 *
 * Copyright (c) 2013 Takuto Wada
 * Licensed under the MIT license.
 *   https://raw.github.com/twada/power-assert/master/MIT-LICENSE.txt
 */
(function (root, factory) {
    'use strict';

    // using returnExports UMD pattern
    if (typeof define === 'function' && define.amd) {
        define(['assert', 'empower'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('assert'), require('empower'));
    } else {
        root.assert = factory(root.assert, root.empower);
    }
}(this, function (baseAssert, empower) {
    'use strict';

    return empower(baseAssert);
}));
<|endoftext|>


<commit_before>var log = require('../logger.js');
var pathLib = require('path');
var Hoek = require('hoek');
var spawnValidator = require('pasties-validator');
var os = require('os');

function resolve(url) {
    var args = [__dirname, '..', '..'].concat(url.split('/'));
    var result = pathLib.join.apply(null, args);
    return pathLib.resolve(result);
}

var options = {
    hooks: {
        errors: true,
        har: true,
        log: true,
        actions: resolve('lib/report/hook/actions.js'),
        css: resolve('lib/report/hook/css.js'),
        images: resolve('lib/report/hook/images.js'),
        timers: resolve('lib/report/hook/timers.js'),
        jquery: resolve('lib/report/hook/jquery.js'),
        pasties: resolve('lib/report/hook/pasties.js')
    },
    validators: {
        errors: true,
        har: true,
        log: true,
        css: resolve('lib/report/validator/css.js'),
        images: resolve('lib/report/validator/images.js'),
        timers: resolve('lib/report/validator/timers.js'),
        jquery: resolve('lib/report/validator/jquery.js'),
        pasties: resolve('lib/report/validator/pasties.js'),
        sizes: resolve('lib/report/validator/sizes.js')
    },
    pageRunTime: 10000
};

var resolveFilepathsToOptions = {
    parentUrl: 'lib/report/resources/parent.html',
    managerScriptPath: 'node_modules/pasties-js/target/pasties-js/js/pasties/mobile.js',
    iframeUrl: 'node_modules/pasties-js/target/pasties-js/html/pasties/mobile.htm',
    managerInitPath: 'lib/report/resources/manager.js'
};

Object.keys(resolveFilepathsToOptions).forEach(function (key) {
    options[key] = resolve(resolveFilepathsToOptions[key]);
});

module.exports = function (data, callback) {
    if (!data || !data.url) {
        if (callback){
            return callback({message: 'Missing phantom url'}, null);
        } else {
            throw new Error('Missing callback');
        }
    }
    var opts = Hoek.clone(options);

    opts.id         = data.id;
    opts.scriptUrl  = data.url;
    opts.target     = data.target;

    opts.outputDirectory = pathLib.resolve(pathLib.join(os.tmpdir(), opts.id));

    log.info('validator processing', opts.id, 'output to', opts.outputDirectory);
    spawnValidator(opts, callback);
};
<commit_msg>add script to hooks<commit_after>var log = require('../logger.js');
var pathLib = require('path');
var Hoek = require('hoek');
var spawnValidator = require('pasties-validator');
var os = require('os');

function resolve(url) {
    var args = [__dirname, '..', '..'].concat(url.split('/'));
    var result = pathLib.join.apply(null, args);
    return pathLib.resolve(result);
}

var options = {
    hooks: {
        errors: true,
        har: true,
        log: true,
        actions: resolve('lib/report/hook/actions.js'),
        css: resolve('lib/report/hook/css.js'),
        script: resolve('lib/report/hook/script.js'),
        images: resolve('lib/report/hook/images.js'),
        timers: resolve('lib/report/hook/timers.js'),
        jquery: resolve('lib/report/hook/jquery.js'),
        pasties: resolve('lib/report/hook/pasties.js')
    },
    validators: {
        errors: true,
        har: true,
        log: true,
        css: resolve('lib/report/validator/css.js'),
        images: resolve('lib/report/validator/images.js'),
        timers: resolve('lib/report/validator/timers.js'),
        jquery: resolve('lib/report/validator/jquery.js'),
        pasties: resolve('lib/report/validator/pasties.js'),
        sizes: resolve('lib/report/validator/sizes.js')
    },
    pageRunTime: 10000
};

var resolveFilepathsToOptions = {
    parentUrl: 'lib/report/resources/parent.html',
    managerScriptPath: 'node_modules/pasties-js/target/pasties-js/js/pasties/mobile.js',
    iframeUrl: 'node_modules/pasties-js/target/pasties-js/html/pasties/mobile.htm',
    managerInitPath: 'lib/report/resources/manager.js'
};

Object.keys(resolveFilepathsToOptions).forEach(function (key) {
    options[key] = resolve(resolveFilepathsToOptions[key]);
});

module.exports = function (data, callback) {
    if (!data || !data.url) {
        if (callback){
            return callback({message: 'Missing phantom url'}, null);
        } else {
            throw new Error('Missing callback');
        }
    }
    var opts = Hoek.clone(options);

    opts.id         = data.id;
    opts.scriptUrl  = data.url;
    opts.target     = data.target;

    opts.outputDirectory = pathLib.resolve(pathLib.join(os.tmpdir(), opts.id));

    log.info('validator processing', opts.id, 'output to', opts.outputDirectory);
    spawnValidator(opts, callback);
};
<|endoftext|>


<commit_before>(function (root, factory) {
    'use strict';
    /* global define, module, require */
    if (typeof define === 'function' && define.amd) { // AMD
        define(['./utils'], factory);
    } else if (typeof exports === 'object') { // Node, browserify and alike
        module.exports = factory(require('./utils'));
    } else { // Browser globals (root is window)
        root.rpgTools = (root.rpgTools || {});
        root.rpgTools.requirements = factory(root.rpgTools.utils);
    }
}(this, function (utils) {
    'use strict';

    /**
     * Does the wearer met this requirements?
     * @param {Object} wearer
     * @param {Object} requirements Each field could be one of following: a) array of possible values, b) object with `min` and/or `max` properties (either numerical or string) or c) primitive value. Missing property means fail.
     * @returns {Boolean}
     */
     function met (wearer, requirements) {
        if (!requirements) {
            return true;
        }

        if (!utils.isObject(wearer)) {
            throw new TypeError('Invalid wearer');
        }

        var result = true;
        var requirementNames = Object.keys(requirements);
        var reqName;
        var req;

        if (!requirementNames.length) { // fast return, empty requirements mean "anything goes"
            return true;
        }

        for (var i = 0; i < requirementNames.length; i++) {
            reqName = requirementNames[i];
            req = requirements[reqName];

            if (wearer[reqName] === undefined) {
                result = false;
                break;
            }

            if (Array.isArray(req)) { // wearer[reqName] should belong to set
                if (req.indexOf(wearer[reqName]) === -1) {
                    result = false;
                    break;
                }
            } else if (utils.isObject(req)) { // wearer[reqName] should be greater than min and/or less than max
                if (req.min && req.min > wearer[reqName]) {
                    result = false;
                    break;
                }

                if (req.max && req.max < wearer[reqName]) {
                    result = false;
                    break;
                }
            } else { // wearer[reqName] should be exactly equal
                if (req !== wearer[reqName]) {
                    result = false;
                    break;
                }
            }
        }

        return result;
    }

    var exports = {
        met: met
    };

    return exports;
}));<commit_msg>tiny optimization for requirements.met<commit_after>(function (root, factory) {
    'use strict';
    /* global define, module, require */
    if (typeof define === 'function' && define.amd) { // AMD
        define(['./utils'], factory);
    } else if (typeof exports === 'object') { // Node, browserify and alike
        module.exports = factory(require('./utils'));
    } else { // Browser globals (root is window)
        root.rpgTools = (root.rpgTools || {});
        root.rpgTools.requirements = factory(root.rpgTools.utils);
    }
}(this, function (utils) {
    'use strict';

    /**
     * Does the wearer met this requirements?
     * @param {Object} wearer
     * @param {Object} requirements Each field could be one of following: a) array of possible values, b) object with `min` and/or `max` properties (either numerical or string) or c) primitive value. Missing property means fail.
     * @returns {Boolean}
     */
     function met (wearer, requirements) {
        if (!requirements) {
            return true;
        }

        if (!utils.isObject(wearer)) {
            throw new TypeError('Invalid wearer');
        }

        var requirementNames = Object.keys(requirements);
        if (!requirementNames.length) { // fast return, empty requirements mean "anything goes"
            return true;
        }

        var result = true;
        var reqName;
        var req;

        for (var i = 0; i < requirementNames.length; i++) {
            reqName = requirementNames[i];
            req = requirements[reqName];

            if (wearer[reqName] === undefined) {
                result = false;
                break;
            }

            if (Array.isArray(req)) { // wearer[reqName] should belong to set
                if (req.indexOf(wearer[reqName]) === -1) {
                    result = false;
                    break;
                }
            } else if (utils.isObject(req)) { // wearer[reqName] should be greater than min and/or less than max
                if (req.min && req.min > wearer[reqName]) {
                    result = false;
                    break;
                }

                if (req.max && req.max < wearer[reqName]) {
                    result = false;
                    break;
                }
            } else { // wearer[reqName] should be exactly equal
                if (req !== wearer[reqName]) {
                    result = false;
                    break;
                }
            }
        }

        return result;
    }

    var exports = {
        met: met
    };

    return exports;
}));<|endoftext|>


<commit_before>var url = require("url");
var fs = require("fs");
var http = require("http");
var resource = require("./resource");
var scriptInjectionProcessor = require("./processors/script-injector");
var proxyMiddleware = require("./http-proxy");
var busterPromise = require("buster-promise");

module.exports = {
    create: function (data, busterResources) {
        var error = module.exports.validate(data);
        if (error) throw new Error(error);
        var resourceSet = Object.create(this);

        resourceSet.busterResources = busterResources;
        resourceSet.resources = {};
        resourceSet.load = data.load || [];
        resourceSet.rootResource = data.rootResource;
        resourceSet.contextPath = data.contextPath || "";

        for (var key in data.resources) {
            resourceSet.addResource(key, data.resources[key]);
        }

        setUpRootResource.call(resourceSet);

        return resourceSet;
    },

    addResource: function (path, data) {
        var self = this;
        var r;

        if ("etag" in data && !("content" in data)) {
            r = this.busterResources.getResourceForPathWithEtag(path, data.etag);
        } else {
            r = resource.create(path, data);
        }

        this.resources[path] = r;

        if (path == "/") {
            if (!("Content-Type" in r.headers)) {
                r.headers["Content-Type"] = "text/html";
            }

            var p = Object.create(scriptInjectionProcessor);
            p.scripts = function () { return rootResourceScripts.call(self); };
            r.addProcessor(p);
        }
    },

    addFile: function (path) {
        this.addResource(path, {
            content: function (promise) {
                fs.readFile(path, function (err, data) {
                    if (err) {
                        promise.reject(err);
                    } else {
                        promise.resolve(data);
                    }
                });
            }
        });
    },

    prependToLoad: function (toPrepend) {
        var msg = validators.loadRequiresResourceExistence({load: toPrepend, resources: this.resources})
        if (msg) throw new Error(msg);

        this.load = toPrepend.concat(this.load);
    },

    /*
     * Takes the data for a resource set. Returns a string with an error message, or
     * nothing if there was no error.
     */
    validate: function (data) {
        for (var validator in validators) {
            var msg = validators[validator](data);
            if (msg) return msg;
        }
    },

    getResource: function (requestedPath, cb) {
        var path;
        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                getBackendContent.call(this, resource, requestedPath, cb)
                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, cb);
                return true;
            }

            if (getResourceContent.call(this, resource, cb)) {
                return true;
            }

        }
    },

    getResourceViaHttp: function (req, res) {
        var requestedPath = req.url;
        var path;

        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                if (!resource.proxy) {
                    var parsed = url.parse(resource.backend);
                    resource.proxy = proxyMiddleware.create(
                        parsed.hostname, parsed.port, parsed.path
                    );
                    resource.proxy.proxyPath = this.contextPath;
                }

                resource.proxy.respond(req, res);

                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, function (err, resource) {
                    if (err) {
                        // TODO: implement me
                    } else {
                        res.writeHead(200, resource.headers);
                        res.write(resource.content);
                        res.end();
                    }
                });
                return true;
            }

            if (getResourceContent.call(this, resource, function (err, resource) {
                if (err) {
                    // TODO: implement me
                } else {
                    res.writeHead(200, resource.headers);
                    res.write(resource.content);
                    res.end();
                }
            })) {
                return true;
            }
        }
    }
};

function rootResourceScripts() {
    var scripts = [];

    for (var i = 0, ii = this.load.length; i < ii; i++) {
        scripts.push(this.contextPath + this.load[i]);
    }

    return scripts;
};

function setUpRootResource() {
    if (!("/" in this.resources)) {
        this.addResource("/", {
            content: "<!DOCTYPE html>"
        });
    }
};

function getReadOnlyResourceWithContent(resource, content) {
    return {
        content: content,
        headers: resource.getHeaders()
    }
};

function getResourceContent(resource, cb) {
    var self = this;

    resource.getContent().then(function (content) {
        cb(undefined, getReadOnlyResourceWithContent.call(self, resource, content));
    }, function (err) {
        cb(err);
    });

    return true;
};

function backendCanHandlePath(resource, requestedPath) {
    return resource.backend
        && requestedPath.slice(0, resource.path.length) == resource.path;
};

function getBackendContent(resource, requestedPath, cb) {
    var self = this;
    var parsed = url.parse(resource.backend);

    var req = http.request({
        method: "GET", path: requestedPath,
        host: parsed.hostname, port: parsed.port
    }, function (res) {
        var content = new Buffer(0);
        res.on("data", function (data) {
            var contentLength = content.length;
            newContent = new Buffer(contentLength + data.length);
            content.copy(newContent, 0);
            data.copy(newContent, contentLength);
            content = newContent;
        });
        res.on("end", function () {
            cb(undefined, {
                headers: res.headers,
                content: content
            });
        });
    });
    req.end();
};

// TODO: fetch resources in parallel and write fancy code to make sure
// they're concatinated in the correct order. We want parallel since
// one or more of the resources in the list may perform HTTP requests.
function combineResources(resource, cb, resourcePaths, content) {
    var self = this;
    if (arguments.length == 2) {
        resourcePaths = resource.combine.slice(0);
        content = "";
    }

    if (resourcePaths.length == 0) {
        resource.content = content;
        getResourceContent.call(this, resource, cb);
        return;
    }

    var path = resourcePaths.shift();
    this.getResource(path, function (err, readOnlyResource) {
        if (err) {
            cb(err);
        } else {
            content += readOnlyResource.content + "\n";
            combineResources.call(self, resource, cb, resourcePaths, content);
        }
    });
};

var validators = {
    resourcesRequired: function (data) {
        if (!data.hasOwnProperty("resources")) {
            return "Missing property 'resources'.";
        }
    },

    loadRequiresResourceExistence: function (data) {
        if (data.hasOwnProperty("load")) {
            for (var i = 0, ii = data.load.length; i < ii; i++) {
                var resourceFound = false;
                for (var resource in data.resources) {
                    if (data.load[i] == resource) resourceFound = true;
                }

                if (!resourceFound) {
                    return "'load' entry '" + data.load[i] + "' missing corresponding 'resources' entry.";
                }
            }
        }
    },

    stuff: function (data) {
        for (var resource in data.resources) {
            if (typeof(data.resources[resource]) == "string") continue;

            if ("content" in data.resources[resource]) {
                if (data.resources[resource].content instanceof Buffer) {
                    continue;
                }

                if (typeof(data.resources[resource].content) == "string") {
                    continue;
                } else {
                    return "The resource '" + resource + "' was not a string."
                }
            }
        }
    }
}<commit_msg>Not requiring the presence of the 'resources' property.<commit_after>var url = require("url");
var fs = require("fs");
var http = require("http");
var resource = require("./resource");
var scriptInjectionProcessor = require("./processors/script-injector");
var proxyMiddleware = require("./http-proxy");
var busterPromise = require("buster-promise");

module.exports = {
    create: function (data, busterResources) {
        var error = module.exports.validate(data);
        if (error) throw new Error(error);
        var resourceSet = Object.create(this);

        resourceSet.busterResources = busterResources;
        resourceSet.resources = {};
        resourceSet.load = data.load || [];
        resourceSet.rootResource = data.rootResource;
        resourceSet.contextPath = data.contextPath || "";

        for (var key in data.resources) {
            resourceSet.addResource(key, data.resources[key]);
        }

        setUpRootResource.call(resourceSet);

        return resourceSet;
    },

    addResource: function (path, data) {
        var self = this;
        var r;

        if ("etag" in data && !("content" in data)) {
            r = this.busterResources.getResourceForPathWithEtag(path, data.etag);
        } else {
            r = resource.create(path, data);
        }

        this.resources[path] = r;

        if (path == "/") {
            if (!("Content-Type" in r.headers)) {
                r.headers["Content-Type"] = "text/html";
            }

            var p = Object.create(scriptInjectionProcessor);
            p.scripts = function () { return rootResourceScripts.call(self); };
            r.addProcessor(p);
        }
    },

    addFile: function (path) {
        this.addResource(path, {
            content: function (promise) {
                fs.readFile(path, function (err, data) {
                    if (err) {
                        promise.reject(err);
                    } else {
                        promise.resolve(data);
                    }
                });
            }
        });
    },

    prependToLoad: function (toPrepend) {
        var msg = validators.loadRequiresResourceExistence({load: toPrepend, resources: this.resources})
        if (msg) throw new Error(msg);

        this.load = toPrepend.concat(this.load);
    },

    /*
     * Takes the data for a resource set. Returns a string with an error message, or
     * nothing if there was no error.
     */
    validate: function (data) {
        for (var validator in validators) {
            var msg = validators[validator](data);
            if (msg) return msg;
        }
    },

    getResource: function (requestedPath, cb) {
        var path;
        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                getBackendContent.call(this, resource, requestedPath, cb)
                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, cb);
                return true;
            }

            if (getResourceContent.call(this, resource, cb)) {
                return true;
            }

        }
    },

    getResourceViaHttp: function (req, res) {
        var requestedPath = req.url;
        var path;

        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                if (!resource.proxy) {
                    var parsed = url.parse(resource.backend);
                    resource.proxy = proxyMiddleware.create(
                        parsed.hostname, parsed.port, parsed.path
                    );
                    resource.proxy.proxyPath = this.contextPath;
                }

                resource.proxy.respond(req, res);

                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, function (err, resource) {
                    if (err) {
                        // TODO: implement me
                    } else {
                        res.writeHead(200, resource.headers);
                        res.write(resource.content);
                        res.end();
                    }
                });
                return true;
            }

            if (getResourceContent.call(this, resource, function (err, resource) {
                if (err) {
                    // TODO: implement me
                } else {
                    res.writeHead(200, resource.headers);
                    res.write(resource.content);
                    res.end();
                }
            })) {
                return true;
            }
        }
    }
};

function rootResourceScripts() {
    var scripts = [];

    for (var i = 0, ii = this.load.length; i < ii; i++) {
        scripts.push(this.contextPath + this.load[i]);
    }

    return scripts;
};

function setUpRootResource() {
    if (!("/" in this.resources)) {
        this.addResource("/", {
            content: "<!DOCTYPE html>"
        });
    }
};

function getReadOnlyResourceWithContent(resource, content) {
    return {
        content: content,
        headers: resource.getHeaders()
    }
};

function getResourceContent(resource, cb) {
    var self = this;

    resource.getContent().then(function (content) {
        cb(undefined, getReadOnlyResourceWithContent.call(self, resource, content));
    }, function (err) {
        cb(err);
    });

    return true;
};

function backendCanHandlePath(resource, requestedPath) {
    return resource.backend
        && requestedPath.slice(0, resource.path.length) == resource.path;
};

function getBackendContent(resource, requestedPath, cb) {
    var self = this;
    var parsed = url.parse(resource.backend);

    var req = http.request({
        method: "GET", path: requestedPath,
        host: parsed.hostname, port: parsed.port
    }, function (res) {
        var content = new Buffer(0);
        res.on("data", function (data) {
            var contentLength = content.length;
            newContent = new Buffer(contentLength + data.length);
            content.copy(newContent, 0);
            data.copy(newContent, contentLength);
            content = newContent;
        });
        res.on("end", function () {
            cb(undefined, {
                headers: res.headers,
                content: content
            });
        });
    });
    req.end();
};

// TODO: fetch resources in parallel and write fancy code to make sure
// they're concatinated in the correct order. We want parallel since
// one or more of the resources in the list may perform HTTP requests.
function combineResources(resource, cb, resourcePaths, content) {
    var self = this;
    if (arguments.length == 2) {
        resourcePaths = resource.combine.slice(0);
        content = "";
    }

    if (resourcePaths.length == 0) {
        resource.content = content;
        getResourceContent.call(this, resource, cb);
        return;
    }

    var path = resourcePaths.shift();
    this.getResource(path, function (err, readOnlyResource) {
        if (err) {
            cb(err);
        } else {
            content += readOnlyResource.content + "\n";
            combineResources.call(self, resource, cb, resourcePaths, content);
        }
    });
};

var validators = {
    loadRequiresResourceExistence: function (data) {
        if (data.hasOwnProperty("load")) {
            for (var i = 0, ii = data.load.length; i < ii; i++) {
                var resourceFound = false;
                for (var resource in data.resources) {
                    if (data.load[i] == resource) resourceFound = true;
                }

                if (!resourceFound) {
                    return "'load' entry '" + data.load[i] + "' missing corresponding 'resources' entry.";
                }
            }
        }
    },

    stuff: function (data) {
        if (!("resources" in data)) return;

        for (var resource in data.resources) {
            if (typeof(data.resources[resource]) == "string") continue;

            if ("content" in data.resources[resource]) {
                if (data.resources[resource].content instanceof Buffer) {
                    continue;
                }

                if (typeof(data.resources[resource].content) == "string") {
                    continue;
                } else {
                    return "The resource '" + resource + "' was not a string."
                }
            }
        }
    }
}<|endoftext|>


<commit_before>var url = require("url");
var fs = require("fs");
var http = require("http");
var resource = require("./resource");
var scriptInjectionProcessor = require("./processors/script-injector");
var proxyMiddleware = require("./http-proxy");
var busterPromise = require("buster-promise");

module.exports = {
    create: function (data, busterResources) {
        var error = module.exports.validate(data);
        if (error) throw new Error(error);
        var resourceSet = Object.create(this);

        resourceSet.busterResources = busterResources;
        resourceSet.resources = {};
        resourceSet.load = data.load || [];
        resourceSet.rootResource = data.rootResource;
        resourceSet.contextPath = data.contextPath || "";

        for (var key in data.resources) {
            resourceSet.addResource(key, data.resources[key]);
        }

        setUpRootResource.call(resourceSet);

        return resourceSet;
    },

    addResource: function (path, data) {
        var self = this;
        var r;

        if ("etag" in data && !("content" in data)) {
            r = this.busterResources.getResourceForPathWithEtag(path, data.etag);
        } else {
            r = resource.create(path, data);
        }

        this.resources[path] = r;

        if (path == "/") {
            if (!("Content-Type" in r.headers)) {
                r.headers["Content-Type"] = "text/html";
            }

            var p = Object.create(scriptInjectionProcessor);
            p.scripts = function () { return rootResourceScripts.call(self); };
            r.addProcessor(p);
        }
    },

    addFile: function (path) {
        this.addResource(path, {
            content: function (promise) {
                fs.readFile(path, function (err, data) {
                    if (err) {
                        promise.reject(err);
                    } else {
                        promise.resolve(data);
                    }
                });
            }
        });
    },

    prependToLoad: function (toPrepend) {
        var msg = validators.loadRequiresResourceExistence({load: toPrepend, resources: this.resources})
        if (msg) throw new Error(msg);

        this.load = toPrepend.concat(this.load);
    },

    /*
     * Takes the data for a resource set. Returns a string with an error message, or
     * nothing if there was no error.
     */
    validate: function (data) {
        for (var validator in validators) {
            var msg = validators[validator](data);
            if (msg) return msg;
        }
    },

    getResource: function (requestedPath, cb) {
        var path;
        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                getBackendContent.call(this, resource, requestedPath, cb)
                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, cb);
                return true;
            }

            if (getResourceContent.call(this, resource, cb)) {
                return true;
            }

        }
    },

    getResourceViaHttp: function (req, res) {
        var requestedPath = req.url;
        var path;

        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                if (!resource.proxy) {
                    var parsed = url.parse(resource.backend);
                    resource.proxy = proxyMiddleware.create(
                        parsed.hostname, parsed.port, parsed.path
                    );
                    resource.proxy.proxyPath = this.contextPath;
                }

                resource.proxy.respond(req, res);

                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, function (err, resource) {
                    if (err) {
                        // TODO: implement me
                    } else {
                        res.writeHead(200, resource.headers);
                        res.write(resource.content);
                        res.end();
                    }
                });
                return true;
            }

            if (getResourceContent.call(this, resource, function (err, resource) {
                if (err) {
                    // TODO: implement me
                } else {
                    res.writeHead(200, resource.headers);
                    res.write(resource.content);
                    res.end();
                }
            })) {
                return true;
            }
        }
    }
};

function rootResourceScripts() {
    var scripts = [];

    for (var i = 0, ii = this.load.length; i < ii; i++) {
        scripts.push(this.contextPath + this.load[i]);
    }

    return scripts;
};

function setUpRootResource() {
    if (!("/" in this.resources)) {
        this.addResource("/", {
            content: "<!DOCTYPE html>"
        });
    }
};

function getReadOnlyResourceWithContent(resource, content) {
    return {
        content: content,
        headers: resource.getHeaders()
    }
};

function getResourceContent(resource, cb) {
    var self = this;

    resource.getContent().then(function (content) {
        cb(undefined, getReadOnlyResourceWithContent.call(self, resource, content));
    }, function (err) {
        cb(err);
    });

    return true;
};

function backendCanHandlePath(resource, requestedPath) {
    return resource.backend
        && requestedPath.slice(0, resource.path.length) == resource.path;
};

function getBackendContent(resource, requestedPath, cb) {
    var self = this;
    var parsed = url.parse(resource.backend);

    var req = http.request({
        method: "GET", path: requestedPath,
        host: parsed.hostname, port: parsed.port
    }, function (res) {
        var content = new Buffer(0);
        res.on("data", function (data) {
            var contentLength = content.length;
            newContent = new Buffer(contentLength + data.length);
            content.copy(newContent, 0);
            data.copy(newContent, contentLength);
            content = newContent;
        });
        res.on("end", function () {
            cb(undefined, {
                headers: res.headers,
                content: content
            });
        });
    });
    req.end();
};

// TODO: fetch resources in parallel and write fancy code to make sure
// they're concatinated in the correct order. We want parallel since
// one or more of the resources in the list may perform HTTP requests.
function combineResources(resource, cb, resourcePaths, content) {
    var self = this;
    if (arguments.length == 2) {
        resourcePaths = resource.combine.slice(0);
        content = "";
    }

    if (resourcePaths.length == 0) {
        resource.content = content;
        getResourceContent.call(this, resource, cb);
        return;
    }

    var path = resourcePaths.shift();
    this.getResource(path, function (err, readOnlyResource) {
        if (err) {
            cb(err);
        } else {
            content += readOnlyResource.content + "\n";
            combineResources.call(self, resource, cb, resourcePaths, content);
        }
    });
};

var validators = {
    loadRequiresResourceExistence: function (data) {
        if (data.hasOwnProperty("load")) {
            for (var i = 0, ii = data.load.length; i < ii; i++) {
                var resourceFound = false;
                for (var resource in data.resources) {
                    if (data.load[i] == resource) resourceFound = true;
                }

                if (!resourceFound) {
                    return "'load' entry '" + data.load[i] + "' missing corresponding 'resources' entry.";
                }
            }
        }
    },

    stuff: function (data) {
        if (!("resources" in data)) return;

        for (var resource in data.resources) {
            if ("content" in data.resources[resource]) {
                if (data.resources[resource].content instanceof Buffer) {
                    continue;
                }

                if (typeof(data.resources[resource].content) == "string") {
                    continue;
                } else {
                    return "The resource '" + resource + "' was not a string."
                }
            }
        }
    }
}<commit_msg>Returning instead of wrapping in if statement.<commit_after>var url = require("url");
var fs = require("fs");
var http = require("http");
var resource = require("./resource");
var scriptInjectionProcessor = require("./processors/script-injector");
var proxyMiddleware = require("./http-proxy");
var busterPromise = require("buster-promise");

module.exports = {
    create: function (data, busterResources) {
        var error = module.exports.validate(data);
        if (error) throw new Error(error);
        var resourceSet = Object.create(this);

        resourceSet.busterResources = busterResources;
        resourceSet.resources = {};
        resourceSet.load = data.load || [];
        resourceSet.rootResource = data.rootResource;
        resourceSet.contextPath = data.contextPath || "";

        for (var key in data.resources) {
            resourceSet.addResource(key, data.resources[key]);
        }

        setUpRootResource.call(resourceSet);

        return resourceSet;
    },

    addResource: function (path, data) {
        var self = this;
        var r;

        if ("etag" in data && !("content" in data)) {
            r = this.busterResources.getResourceForPathWithEtag(path, data.etag);
        } else {
            r = resource.create(path, data);
        }

        this.resources[path] = r;

        if (path == "/") {
            if (!("Content-Type" in r.headers)) {
                r.headers["Content-Type"] = "text/html";
            }

            var p = Object.create(scriptInjectionProcessor);
            p.scripts = function () { return rootResourceScripts.call(self); };
            r.addProcessor(p);
        }
    },

    addFile: function (path) {
        this.addResource(path, {
            content: function (promise) {
                fs.readFile(path, function (err, data) {
                    if (err) {
                        promise.reject(err);
                    } else {
                        promise.resolve(data);
                    }
                });
            }
        });
    },

    prependToLoad: function (toPrepend) {
        var msg = validators.loadRequiresResourceExistence({load: toPrepend, resources: this.resources})
        if (msg) throw new Error(msg);

        this.load = toPrepend.concat(this.load);
    },

    /*
     * Takes the data for a resource set. Returns a string with an error message, or
     * nothing if there was no error.
     */
    validate: function (data) {
        for (var validator in validators) {
            var msg = validators[validator](data);
            if (msg) return msg;
        }
    },

    getResource: function (requestedPath, cb) {
        var path;
        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                getBackendContent.call(this, resource, requestedPath, cb)
                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, cb);
                return true;
            }

            if (getResourceContent.call(this, resource, cb)) {
                return true;
            }

        }
    },

    getResourceViaHttp: function (req, res) {
        var requestedPath = req.url;
        var path;

        for (path in this.resources) {
            var resource = this.resources[path];

            if (backendCanHandlePath.call(this, resource, requestedPath)) {
                if (!resource.proxy) {
                    var parsed = url.parse(resource.backend);
                    resource.proxy = proxyMiddleware.create(
                        parsed.hostname, parsed.port, parsed.path
                    );
                    resource.proxy.proxyPath = this.contextPath;
                }

                resource.proxy.respond(req, res);

                return true;
            }

            if (this.contextPath + path != requestedPath) continue;

            if (resource.combine && !resource.content) {
                combineResources.call(this, resource, function (err, resource) {
                    if (err) {
                        // TODO: implement me
                    } else {
                        res.writeHead(200, resource.headers);
                        res.write(resource.content);
                        res.end();
                    }
                });
                return true;
            }

            if (getResourceContent.call(this, resource, function (err, resource) {
                if (err) {
                    // TODO: implement me
                } else {
                    res.writeHead(200, resource.headers);
                    res.write(resource.content);
                    res.end();
                }
            })) {
                return true;
            }
        }
    }
};

function rootResourceScripts() {
    var scripts = [];

    for (var i = 0, ii = this.load.length; i < ii; i++) {
        scripts.push(this.contextPath + this.load[i]);
    }

    return scripts;
};

function setUpRootResource() {
    if (!("/" in this.resources)) {
        this.addResource("/", {
            content: "<!DOCTYPE html>"
        });
    }
};

function getReadOnlyResourceWithContent(resource, content) {
    return {
        content: content,
        headers: resource.getHeaders()
    }
};

function getResourceContent(resource, cb) {
    var self = this;

    resource.getContent().then(function (content) {
        cb(undefined, getReadOnlyResourceWithContent.call(self, resource, content));
    }, function (err) {
        cb(err);
    });

    return true;
};

function backendCanHandlePath(resource, requestedPath) {
    return resource.backend
        && requestedPath.slice(0, resource.path.length) == resource.path;
};

function getBackendContent(resource, requestedPath, cb) {
    var self = this;
    var parsed = url.parse(resource.backend);

    var req = http.request({
        method: "GET", path: requestedPath,
        host: parsed.hostname, port: parsed.port
    }, function (res) {
        var content = new Buffer(0);
        res.on("data", function (data) {
            var contentLength = content.length;
            newContent = new Buffer(contentLength + data.length);
            content.copy(newContent, 0);
            data.copy(newContent, contentLength);
            content = newContent;
        });
        res.on("end", function () {
            cb(undefined, {
                headers: res.headers,
                content: content
            });
        });
    });
    req.end();
};

// TODO: fetch resources in parallel and write fancy code to make sure
// they're concatinated in the correct order. We want parallel since
// one or more of the resources in the list may perform HTTP requests.
function combineResources(resource, cb, resourcePaths, content) {
    var self = this;
    if (arguments.length == 2) {
        resourcePaths = resource.combine.slice(0);
        content = "";
    }

    if (resourcePaths.length == 0) {
        resource.content = content;
        getResourceContent.call(this, resource, cb);
        return;
    }

    var path = resourcePaths.shift();
    this.getResource(path, function (err, readOnlyResource) {
        if (err) {
            cb(err);
        } else {
            content += readOnlyResource.content + "\n";
            combineResources.call(self, resource, cb, resourcePaths, content);
        }
    });
};

var validators = {
    loadRequiresResourceExistence: function (data) {
        if (!("load" in data)) return;

        for (var i = 0, ii = data.load.length; i < ii; i++) {
            var resourceFound = false;
            for (var resource in data.resources) {
                if (data.load[i] == resource) resourceFound = true;
            }

            if (!resourceFound) {
                return "'load' entry '" + data.load[i] + "' missing corresponding 'resources' entry.";
            }
        }
    },

    stuff: function (data) {
        if (!("resources" in data)) return;

        for (var resource in data.resources) {
            if ("content" in data.resources[resource]) {
                if (data.resources[resource].content instanceof Buffer) {
                    continue;
                }

                if (typeof(data.resources[resource].content) == "string") {
                    continue;
                } else {
                    return "The resource '" + resource + "' was not a string."
                }
            }
        }
    }
}<|endoftext|>


<commit_before>'use strict';

var express = require('express');
var config = require('../user/config-user');
var userAuth = require('./mbaas-auth');
var sessionMiddleware = require('./mbaas-session-middleware');
var _ = require('lodash');

function initRouter(mediator, authResponseExclusionList, expressSessionMiddleware) {
  var router = express.Router();

  router.all('/auth', function(req, res) {
    var params = req.body;
    var userId = params && params.userId || params.username;

    //If there is no userId, then we cannot authenticate.
    if (!userId) {
      console.log('No username provided');
      res.status(400);
      return res.json({message: 'Invalid credentials'});
    }

    // try to authenticate
    userAuth.auth(mediator, userId, params.password)
      .then(function(profileData) {
        // trim the authentication response to remove specified fields
        var authResponse = trimAuthResponse(profileData, authResponseExclusionList);
        // on success pass relevant data into response

        //Using express-session to generate and store a session.
        //This is only done for authenticated requests. Otherwise we don't generate a session
        //as it would cause a session to be created for every request.
        expressSessionMiddleware(req, res, function(err) {
          //An error occurred while trying to create a valid session token for the user.
          if (err) {
            return res.status(500).json({message: "Unexpected error when creating a session. Please try again."});
          }

          res.status = 200;
          res.json({
            status: 'ok',
            userId: userId,
            sessionToken: req.sessionID,
            authResponse: authResponse
          });
        });
      })
      .catch(function(err) {
        // on error pass error message into response body, assign 401 http code.
        // 401 - invalid credentials (unauthorised)
        res.status(401);
        res.json(err.message ? err.message : 'Invalid Credentials');
      });
  });

  router.all('/verifysession', sessionMiddleware.verifySession, function(req, res) {
    res.json({
      isValid: req.sessionValid
    });
  });

  router.all('/revokesession', sessionMiddleware.revokeSession, function(req, res) {
    res.json({});
  });

  router.route('/').get(function(req, res) {
    mediator.once('done:wfm:user:list', function(data) {
      res.json(data);
    });
    mediator.publish('wfm:user:list');
  });
  router.route('/:id').get(function(req, res) {
    var userId = req.params.id;
    mediator.once('done:wfm:user:read:' + userId, function(data) {
      res.json(data);
    });
    mediator.publish('wfm:user:read', userId);
  });
  router.route('/:id').put(function(req, res) {
    var userId = req.params.id;
    var user = req.body.user;
    mediator.once('done:wfm:user:update:' + userId, function(saveduser) {
      res.json(saveduser);
    });
    mediator.publish('wfm:user:update', user);
  });
  router.route('/').post(function(req, res) {
    var ts = new Date().getTime();  // TODO: replace this with a proper uniqe (eg. a cuid)
    var user = req.body.user;
    user.createdTs = ts;
    mediator.once('done:wfm:user:create:' + ts, function(createduser) {
      res.json(createduser);
    });
    mediator.publish('wfm:user:create', user, ts);
  });
  router.route('/:id').delete(function(req, res) {
    var userId = req.params.id;
    var user = req.body.user;
    mediator.once('done:wfm:user:delete:' + userId, function(deleted) {
      res.json(deleted);
    });
    mediator.publish('wfm:user:delete', user);
  });
  return router;
}

/**
* Function to trim the authentication response to remove certain fields from being sent.
* By default, the password will be removed from the response.
* @param authResponse {object} - the untrimmed auth response
* @param exclusionList {array} - the array of field names to remove from the authentication response
* @return authResponse {object} - the trimmed authentication response
*/
function trimAuthResponse(authResponse, exclusionList) {
  if (exclusionList === undefined || exclusionList === null) {
    // return a default auth response if the exclusion list is null or undefined
    return _.omit(authResponse, config.defaultAuthResponseExclusionList);
  }
  return _.omit(authResponse, exclusionList);
}

/**
 * Initializes the router, mounting it in the supplied express application
 * @param  {Mediator}   mediator                 Mediator instance from fh-wfm-mediator
 * @param  {Express.App}   app                   Express application
 * @param  {Array}   authResponseExclusionList   List of fields in the User schema to exclude from responses
 * @param  {Object}   sessionOptions             Options for storage and express-session
 * @param  {Function} cb                         Node-style callback
 */
function init(mediator, app, authResponseExclusionList, sessionOptions, cb) {

  sessionMiddleware.init(sessionOptions, function(err, result) {
    if (err) {
      return cb(err);
    }

    //Creating the express-session middleware using the redis or mongo database.
    var expressSessionMiddleware = result.session({
      secret: result.options.config.secret,
      store: result.store,
      resave: result.options.config.resave,
      saveUninitialized: result.options.config.saveUninitialized
    });

    //The express session is only used for authentication responses
    var router = initRouter(mediator, authResponseExclusionList, expressSessionMiddleware);
    app.use(config.apiPath, router);
    return cb();
  });
}

module.exports = {
  init: init,
  trimAuthResponse: trimAuthResponse
};
<commit_msg>status assigment fix<commit_after>'use strict';

var express = require('express');
var config = require('../user/config-user');
var userAuth = require('./mbaas-auth');
var sessionMiddleware = require('./mbaas-session-middleware');
var _ = require('lodash');

function initRouter(mediator, authResponseExclusionList, expressSessionMiddleware) {
  var router = express.Router();

  router.all('/auth', function(req, res) {
    var params = req.body;
    var userId = params && params.userId || params.username;

    //If there is no userId, then we cannot authenticate.
    if (!userId) {
      console.log('No username provided');
      res.status(400);
      return res.json({message: 'Invalid credentials'});
    }

    // try to authenticate
    userAuth.auth(mediator, userId, params.password)
      .then(function(profileData) {
        // trim the authentication response to remove specified fields
        var authResponse = trimAuthResponse(profileData, authResponseExclusionList);
        // on success pass relevant data into response

        //Using express-session to generate and store a session.
        //This is only done for authenticated requests. Otherwise we don't generate a session
        //as it would cause a session to be created for every request.
        expressSessionMiddleware(req, res, function(err) {
          //An error occurred while trying to create a valid session token for the user.
          if (err) {
            return res.status(500).json({message: "Unexpected error when creating a session. Please try again."});
          }

          res.status(200).json({
            status: 'ok',
            userId: userId,
            sessionToken: req.sessionID,
            authResponse: authResponse
          });
        });
      })
      .catch(function(err) {
        // on error pass error message into response body, assign 401 http code.
        // 401 - invalid credentials (unauthorised)
        res.status(401);
        res.json(err.message ? err.message : 'Invalid Credentials');
      });
  });

  router.all('/verifysession', sessionMiddleware.verifySession, function(req, res) {
    res.json({
      isValid: req.sessionValid
    });
  });

  router.all('/revokesession', sessionMiddleware.revokeSession, function(req, res) {
    res.json({});
  });

  router.route('/').get(function(req, res) {
    mediator.once('done:wfm:user:list', function(data) {
      res.json(data);
    });
    mediator.publish('wfm:user:list');
  });
  router.route('/:id').get(function(req, res) {
    var userId = req.params.id;
    mediator.once('done:wfm:user:read:' + userId, function(data) {
      res.json(data);
    });
    mediator.publish('wfm:user:read', userId);
  });
  router.route('/:id').put(function(req, res) {
    var userId = req.params.id;
    var user = req.body.user;
    mediator.once('done:wfm:user:update:' + userId, function(saveduser) {
      res.json(saveduser);
    });
    mediator.publish('wfm:user:update', user);
  });
  router.route('/').post(function(req, res) {
    var ts = new Date().getTime();  // TODO: replace this with a proper uniqe (eg. a cuid)
    var user = req.body.user;
    user.createdTs = ts;
    mediator.once('done:wfm:user:create:' + ts, function(createduser) {
      res.json(createduser);
    });
    mediator.publish('wfm:user:create', user, ts);
  });
  router.route('/:id').delete(function(req, res) {
    var userId = req.params.id;
    var user = req.body.user;
    mediator.once('done:wfm:user:delete:' + userId, function(deleted) {
      res.json(deleted);
    });
    mediator.publish('wfm:user:delete', user);
  });
  return router;
}

/**
* Function to trim the authentication response to remove certain fields from being sent.
* By default, the password will be removed from the response.
* @param authResponse {object} - the untrimmed auth response
* @param exclusionList {array} - the array of field names to remove from the authentication response
* @return authResponse {object} - the trimmed authentication response
*/
function trimAuthResponse(authResponse, exclusionList) {
  if (exclusionList === undefined || exclusionList === null) {
    // return a default auth response if the exclusion list is null or undefined
    return _.omit(authResponse, config.defaultAuthResponseExclusionList);
  }
  return _.omit(authResponse, exclusionList);
}

/**
 * Initializes the router, mounting it in the supplied express application
 * @param  {Mediator}   mediator                 Mediator instance from fh-wfm-mediator
 * @param  {Express.App}   app                   Express application
 * @param  {Array}   authResponseExclusionList   List of fields in the User schema to exclude from responses
 * @param  {Object}   sessionOptions             Options for storage and express-session
 * @param  {Function} cb                         Node-style callback
 */
function init(mediator, app, authResponseExclusionList, sessionOptions, cb) {

  sessionMiddleware.init(sessionOptions, function(err, result) {
    if (err) {
      return cb(err);
    }

    //Creating the express-session middleware using the redis or mongo database.
    var expressSessionMiddleware = result.session({
      secret: result.options.config.secret,
      store: result.store,
      resave: result.options.config.resave,
      saveUninitialized: result.options.config.saveUninitialized
    });

    //The express session is only used for authentication responses
    var router = initRouter(mediator, authResponseExclusionList, expressSessionMiddleware);
    app.use(config.apiPath, router);
    return cb();
  });
}

module.exports = {
  init: init,
  trimAuthResponse: trimAuthResponse
};
<|endoftext|>


<commit_before>// Blog routes
exports.blog = require('./blog');

// Portfolio page
exports.projects = function (req, res) {
  res.render('projects', {
    page: 'projects',
    title: 'Todd Wolfson - Projects',
    // TODO: I really dislike view configuration being placed in the route
    navMargin: false,
    seoKeywords: 'dev tools, web tools, spritesmith, sexy-bash-prompt, File Watcher, jsmin-sourcemap, Find++',
    seoDescription: 'Projects by Todd Wolfson: spritesmith, sexy-bash-prompt, File Watcher, jsmin-sourcemap, Find++'
  });
};

// Contact route
exports.contact = require('./contact');

// Kaleido page
exports.kaleido = function (req, res) {
  // DEV: Relocate js-yaml inside route for dev-only dependencies
  var yaml = require('js-yaml'),
      urls = require('../../test/perceptual-tests/urls');
  res.render('kaleido', {layout: false, urls: urls});
};

// Render a LICENSE page
var fs = require('fs'),
    license = fs.readFileSync(__dirname + '/../../LICENSE-MIT', 'utf8');
exports.license = function (req, res) {
  res.render('license', {
    page: 'license',
    license: license,
    title: 'Todd Wolfson - License',
    seoKeywords: 'license, mit',
    seoDescription: 'twolfson.com uses the MIT License'
  });
};

// Render a health page
var pkg = require('../../package.json'),
    pkgVersion = pkg.version;
exports.health = function (req, res) {
  var retObj = {
        'version': pkgVersion,
        'uptime': process.uptime(),
        'memory': process.memoryUsage(),
        'pid': process.pid
      };
  res.send(retObj);
};

// 404 page (no SEO here)
exports['404'] = function (req, res) {
  res.status(404);
  res.render('404', {'page': '404'});
};<commit_msg>Exposing `env` to /health<commit_after>// Blog routes
exports.blog = require('./blog');

// Portfolio page
exports.projects = function (req, res) {
  res.render('projects', {
    page: 'projects',
    title: 'Todd Wolfson - Projects',
    // TODO: I really dislike view configuration being placed in the route
    navMargin: false,
    seoKeywords: 'dev tools, web tools, spritesmith, sexy-bash-prompt, File Watcher, jsmin-sourcemap, Find++',
    seoDescription: 'Projects by Todd Wolfson: spritesmith, sexy-bash-prompt, File Watcher, jsmin-sourcemap, Find++'
  });
};

// Contact route
exports.contact = require('./contact');

// Kaleido page
exports.kaleido = function (req, res) {
  // DEV: Relocate js-yaml inside route for dev-only dependencies
  var yaml = require('js-yaml'),
      urls = require('../../test/perceptual-tests/urls');
  res.render('kaleido', {layout: false, urls: urls});
};

// Render a LICENSE page
var fs = require('fs'),
    license = fs.readFileSync(__dirname + '/../../LICENSE-MIT', 'utf8');
exports.license = function (req, res) {
  res.render('license', {
    page: 'license',
    license: license,
    title: 'Todd Wolfson - License',
    seoKeywords: 'license, mit',
    seoDescription: 'twolfson.com uses the MIT License'
  });
};

// Render a health page
var pkg = require('../../package.json'),
    pkgVersion = pkg.version,
    config = require('../../config');
exports.health = function (req, res) {
  var retObj = {
        'version': pkgVersion,
        'uptime': process.uptime(),
        'memory': process.memoryUsage(),
        'pid': process.pid,
        'env': env
      };
  res.send(retObj);
};

// 404 page (no SEO here)
exports['404'] = function (req, res) {
  res.status(404);
  res.render('404', {'page': '404'});
};<|endoftext|>


<commit_before>var sorter = require('./sorter');
var canonical = require('./canonical');

module.exports = routes;

function title(suffix) {
  var t = 'Liftie';
  if(suffix && suffix.length) {
    t += ' | ' + suffix;
  }
  return t;
}

function renderResorts(req, res, next) {
  var opts = req.opts || {};

  req.data.get(req.requested, function(err, items) {
    var template = opts.template || 'index';

    if (err) {
      return next(err);
    }
    if (opts.openSingle && items.length === 1) {
      items[0].open = true; // if single resort requested, always mark it as open
      res.locals.title = items[0].name;
      if (template !== 'widget') {
        res.locals.single = true;
      }
    } else {
      items = sorter(items, req.cookies);
    }

    res.render(template, {
      title: title(res.locals.title),
      all: req.data.all(),
      tags: req.data.tags(),
      resorts: items,
    });
  });
}

/**
 * Handles 2 types of URIs:
 *   / - display all resorts
 *   /resort/<name> - display a specific resort
 *   /?resorts=<name1>,<name2>... - display resorts on the list
 */
function index(req, res, next) {
  req.requested = req.params.resort || req.query.resorts;
  req.opts = {
    openSingle: true
  };
  next();
}

/**
 * Display a single resort as a widget
 */
function widget(req, res, next) {
  var requested = req.params.resort;
  req.opts = {
    openSingle: true,
    template: 'widget'
  };
  req.requested = requested.split(',')[0]; // only one resort allowed
  next();
}

/**
 * Handles
 *   /stars - display starred subset of resorts
 */
function stars(req, res, next) {
  req.requested = req.cookies['resorts-starred'];
  res.locals.title = 'Stars';
  next();
}

/**
 * Handles /tag/<tag> URIs
 */
function tag(req, res, next) {
  res.locals.title = req.tag.label;
  next();
}

function about(req, res) {
  res.render('about', {
    title: title('About')
  });
}

function sitemap(req, res) {
  res.contentType('application/xml');
  res.render('sitemap', {
    resorts: req.data.all(true)
  });
}

function absent(req, res, next) {
  req.requested = req.data.filtered(function(resort) {
    var hasLists = resort.lifts && resort.lifts.status && Object.keys(resort.lifts.status).length;
    return !hasLists;
  });
  res.locals.title = 'Absent';
  next();
}

function closed(req, res, next) {
  req.requested = req.data.filtered(function(resort) {
    return resort.opening;
  });
  res.locals.title = 'Closed';
  next();
}

function stats(req, res) {
  var t = req.tag;
  res.render('stats', {
    title: title('Statistics'),
    sectionLink: t ? '/tag/' + t.slug : '/',
    sectionTitle: t ? t.label : 'All Lifts',
    stats: req.data.stats(req.requested)
  });
}

function api(req, res, next) {
  req.data.get(req.params.resort, function(err, resorts) {
    if(err) {
      return next(err);
    }
    if(resorts.length !== 1) {
      return res.send(404, 'Invalid resort name: ' + req.params.resort);
    }
    res.send(resorts[0]);
  });
}

function routes(app) {

  function reqData(req, res, next) {
    req.data = app.data;
    next();
  }

  app.param('tag', function(req, res, next, t) {
    var data = app.data,
      tags = data.tags(),
      requested = tags[t] && tags[t].members;
    if(!requested) {
      t = canonical(t);
      if(tags[t]) {
        // permanent redirect to canonical form of the tag
        return res.redirect(301, '/tag/' + t);
      }
      return res.send(404, 'Invalid tag name: ' + req.params.tag);
    }
    req.requested = requested;
    req.tag = tags[t];
    res.locals.tag = t;
    next();
  });

  app.get('/', reqData, index, renderResorts);
  app.get('/resort/:resort', reqData, index, renderResorts);
  app.get('/widget/resort/:resort', reqData, widget, renderResorts);
  app.get('/tag/:tag', reqData, tag, renderResorts);
  app.get('/stars', reqData, stars, renderResorts);
  app.get('/api/resort/:resort', reqData, api);
  app.get('/sitemap.xml', reqData, sitemap);
  app.get('/about', about);
  app.get('/absent', reqData, absent, renderResorts);
  app.get('/closed', reqData, closed, renderResorts);
  app.get('/stats/:tag', reqData, stats);
  app.get('/stats', reqData, stats);
}<commit_msg>add /confused route<commit_after>var sorter = require('./sorter');
var canonical = require('./canonical');

module.exports = routes;

function title(suffix) {
  var t = 'Liftie';
  if(suffix && suffix.length) {
    t += ' | ' + suffix;
  }
  return t;
}

function renderResorts(req, res, next) {
  var opts = req.opts || {};

  req.data.get(req.requested, function(err, items) {
    var template = opts.template || 'index';

    if (err) {
      return next(err);
    }
    if (opts.openSingle && items.length === 1) {
      items[0].open = true; // if single resort requested, always mark it as open
      res.locals.title = items[0].name;
      if (template !== 'widget') {
        res.locals.single = true;
      }
    } else {
      items = sorter(items, req.cookies);
    }

    res.render(template, {
      title: title(res.locals.title),
      all: req.data.all(),
      tags: req.data.tags(),
      resorts: items,
    });
  });
}

/**
 * Handles 2 types of URIs:
 *   / - display all resorts
 *   /resort/<name> - display a specific resort
 *   /?resorts=<name1>,<name2>... - display resorts on the list
 */
function index(req, res, next) {
  req.requested = req.params.resort || req.query.resorts;
  req.opts = {
    openSingle: true
  };
  next();
}

/**
 * Display a single resort as a widget
 */
function widget(req, res, next) {
  var requested = req.params.resort;
  req.opts = {
    openSingle: true,
    template: 'widget'
  };
  req.requested = requested.split(',')[0]; // only one resort allowed
  next();
}

/**
 * Handles
 *   /stars - display starred subset of resorts
 */
function stars(req, res, next) {
  req.requested = req.cookies['resorts-starred'];
  res.locals.title = 'Stars';
  next();
}

/**
 * Handles /tag/<tag> URIs
 */
function tag(req, res, next) {
  res.locals.title = req.tag.label;
  next();
}

function about(req, res) {
  res.render('about', {
    title: title('About')
  });
}

function sitemap(req, res) {
  res.contentType('application/xml');
  res.render('sitemap', {
    resorts: req.data.all(true)
  });
}

function absent(req, res, next) {
  req.requested = req.data.filtered(function(resort) {
    var hasLists = resort.lifts && resort.lifts.status && Object.keys(resort.lifts.status).length;
    return !hasLists;
  });
  res.locals.title = 'Absent';
  next();
}

function confused(req, res, next) {
  req.requested = req.data.filtered(function(resort) {
    return resort.opening && resort.lifts.stats && resort.lifts.stats.open;
  });
  res.locals.title = 'Confused';
  next();
}

function closed(req, res, next) {
  req.requested = req.data.filtered(function(resort) {
    return resort.opening;
  });
  res.locals.title = 'Closed';
  next();
}

function stats(req, res) {
  var t = req.tag;
  res.render('stats', {
    title: title('Statistics'),
    sectionLink: t ? '/tag/' + t.slug : '/',
    sectionTitle: t ? t.label : 'All Lifts',
    stats: req.data.stats(req.requested)
  });
}

function api(req, res, next) {
  req.data.get(req.params.resort, function(err, resorts) {
    if(err) {
      return next(err);
    }
    if(resorts.length !== 1) {
      return res.send(404, 'Invalid resort name: ' + req.params.resort);
    }
    res.send(resorts[0]);
  });
}

function routes(app) {

  function reqData(req, res, next) {
    req.data = app.data;
    next();
  }

  app.param('tag', function(req, res, next, t) {
    var data = app.data,
      tags = data.tags(),
      requested = tags[t] && tags[t].members;
    if(!requested) {
      t = canonical(t);
      if(tags[t]) {
        // permanent redirect to canonical form of the tag
        return res.redirect(301, '/tag/' + t);
      }
      return res.send(404, 'Invalid tag name: ' + req.params.tag);
    }
    req.requested = requested;
    req.tag = tags[t];
    res.locals.tag = t;
    next();
  });

  app.get('/', reqData, index, renderResorts);
  app.get('/resort/:resort', reqData, index, renderResorts);
  app.get('/widget/resort/:resort', reqData, widget, renderResorts);
  app.get('/tag/:tag', reqData, tag, renderResorts);
  app.get('/stars', reqData, stars, renderResorts);
  app.get('/api/resort/:resort', reqData, api);
  app.get('/sitemap.xml', reqData, sitemap);
  app.get('/about', about);
  app.get('/absent', reqData, absent, renderResorts);
  app.get('/confused', reqData, confused, renderResorts);
  app.get('/closed', reqData, closed, renderResorts);
  app.get('/stats/:tag', reqData, stats);
  app.get('/stats', reqData, stats);
}<|endoftext|>