Source: urls.js

/**
 * @module urls
 */

var url = require('url');
var util = require('util');

/**
 * Function to convert a simple string to a URL macro.
 *
 * Simply uppercases string, and encloses in '${}'
 *
 * Doesn't check for spaces or anything not URL-safe, so just
 * don't give it anything stupid and expect it to work.
 *
 * @type {Function}
 * @param str any string
 */
var stringToMacro = exports.toMacro = function(str){
    return util.format('${%s}', str.toUpperCase());
};

/**
 * Populates macro values in any arbitrary text according to macro-to-string
 * definition and macro value mapping.
 *
 * @type {Function}
 */
var expandURLMacros = exports.expandURLMacros = function(url_string, macro_value_mapping){
    for (var macro in macro_value_mapping) {
        if (macro_value_mapping.hasOwnProperty(macro)){
            var macro_string = stringToMacro(macro);
            url_string = url_string.replace(macro_string, macro_value_mapping[macro]);
        }
    }
    return url_string;
};

/**
 * Abstract class to contain base methods used for constructing & parsing
 * adserver URL's.
 *
 * @param {String} hostname domain & port #, if applicable. Ex: 'localhost:5000' or 'ads.cliquesads.com'
 * @param {String} secure_hostname domain & port #, if applicable. Ex: 'localhost:5000' or 'ads.cliquesads.com'
 * @param {Object} query_schema object containing query parameters expected for a
 *  given URL that specifies the level of encoding / decoding to perform for each
 * @param {String} path path for this URL
 * @param {Number} port port number if applicable, otherwise 'null'
 * @constructor
 * @abstract
 */
var AdServerURL = function(hostname, secure_hostname, path, query_schema, port){
    this.hostname       = hostname;
    this.secure_hostname= secure_hostname;
    this.port           = port;
    this.path           = path;
    //contains query fields to expect & levels of encoding expected for each
    this.query_schema   = query_schema || {};
};

/**
 * Parses & decodes URL query params from raw URL
 *
 * Doesn't actually parse the whole string since Express parses out query params for
 * you into an object, so just pass request.query as query argument.
 *
 * @param {Object} query output of request.query
 * @param {Boolean} secure
 */
AdServerURL.prototype.parse = function(query, secure){
    var self = this;
    self.secure = secure;
    Object.keys(query).forEach(function(item){
        if (query.hasOwnProperty(item)){
            // decode as necessary
            self[item] = query[item];
            for (var d=0; d < self.query_schema[item]; d++){
                self[item] = decodeURIComponent(self[item])
            }
        }
    });
};

/**
 * Lightweight wrapper around url.format to encode any query params which are
 * expected to be encoded, and return fully formed URL.
 *
 * Any query values in `query` arg will be encoded according to this.query_schema
 * and added to the formatted URL.
 *
 * Any query values in query_schema AND NOT in `query` object will be formatted as
 * macros using standard macro delimiters.
 *
 * Stores resulting URL on instance as this.url
 *
 * @param {Object} query object containing a URL's query param keys & values
 * @param {Boolean} secure
 */
AdServerURL.prototype.format = function(query, secure){
    var self = this;
    query = query || {};
    // encode all query params appropriate number of times
    for (var key in self.query_schema){
        if (self.query_schema.hasOwnProperty(key)){
            if (query.hasOwnProperty(key)){
                // if value passed in query object for this key,
                // just encode it however many times we're supposed
                // to according to self.query_schema
                for (var d=0; d < self.query_schema[key]; d++) {
                    query[key] = encodeURIComponent(query[key])
                }
            } else {
                query[key] = stringToMacro(key);
            }
        }
    }
    // This pisses me off royally, but URL format automatically encodes
    // anything encodable found in query object, so have to decode once before
    // returning.
    var encodedURL = url.format({
        protocol: secure ? 'https' : 'http',
        hostname: secure ? self.secure_hostname : self.hostname,
        port: self.port,
        pathname: self.path,
        query: query
    });
    this.url = decodeURIComponent(encodedURL);
    return this.url;
};

/**
 * @const PUB_PATH
 * @type {string}
 */
var PUB_PATH = exports.PUB_PATH = '/pub';
/**
 * Publisher URL subclass
 * @class
 * @type {string}
 */
var PubURL = exports.PubURL = function(hostname, secure_hostname, port){
    var query_schema = {
        pid: 0,
        cb: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, PUB_PATH, query_schema, port);
};
util.inherits(PubURL, AdServerURL);


/**
 * @const IMP_PATH
 * @type {string}
 */
var IMP_PATH = exports.IMP_PATH = '/crg';
/**
 * Impression URL subclass
 * @class
 * @type {string}
 */
var ImpURL = exports.ImpURL = function(hostname, secure_hostname, port){
    var query_schema = {
        crgid: 0,
        pid: 0,
        impid: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, IMP_PATH, query_schema, port);
};
util.inherits(ImpURL, AdServerURL);

/**
 * @const CR_PATH
 * @type {string}
 */
var CR_PATH = exports.CR_PATH = '/cr';
/**
 * Creative Test URL subclass
 *
 * @class
 * @type {string}
 */
var CreativeURL = exports.CreativeURL = function(hostname, secure_hostname, port){
    var query_schema = {
        cid: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, CR_PATH, query_schema, port);
};
util.inherits(CreativeURL, AdServerURL);

/**
 * @const PUBCR_PATH
 * @type {string}
 */
var PUBCR_PATH = exports.PUBCR_PATH = '/pubcr';
/**
 * Publisher Default Creative URL subclass
 * @class
 * @type {string}
 */
var PubCreativeURL = exports.PubCreativeURL = function(hostname, secure_hostname, port){
    var query_schema = {
        pid: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, PUBCR_PATH, query_schema, port);
};
util.inherits(PubCreativeURL, AdServerURL);


/**
 * @const CLICK_PATH
 * @type {string}
 */
var CLICK_PATH = exports.CLICK_PATH = '/clk';
/**
 * Click URL subclass
 * @class
 * @type {string}
 */
var ClickURL = exports.ClickURL = function(hostname, secure_hostname, port){
    var query_schema = {
        cid: 0,
        pid: 0,
        advid: 0,
        crgid: 0,
        campid: 0,
        redir: 1,
        impid: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, CLICK_PATH, query_schema, port);
};
util.inherits(ClickURL, AdServerURL);

/**
 * @const CLICK_PATH
 * @type {string}
 */
var ACTION_PATH = exports.ACTION_PATH = '/ab';
/**
 * Conversion URL subclass
 *
 * @class
 * @type {string}
 */
var ActionBeaconURL = exports.ActionBeaconURL = function(hostname, secure_hostname, port){
    var query_schema = {
        abid: 0,
        value: 0,
        advid: 0
    };
    AdServerURL.call(this, hostname, secure_hostname, ACTION_PATH, query_schema, port);
};
util.inherits(ActionBeaconURL, AdServerURL);