common.js

/**
 * MOST Web Framework
 * A JavaScript Web Framework
 * http://themost.io
 *
 * Copyright (c) 2014, Kyriakos Barbounakis k.barbounakis@gmail.com, Anthi Oikonomou anthioikonomou@gmail.com
 *
 * Released under the BSD3-Clause license
 * Date: 2014-06-09
 */
/**
 * @private
 */
var util = require('util'), errors = require('./http-error-codes.json'), crypto = require('crypto');

/**
 * Abstract Method Exception class
 * @class
 * @augments Error
 * @memberOf module:most-web.common
 * */
function AbstractMethodException(message) {
    AbstractMethodException.super_.call(this, message || 'Cannot call an abstract method.', this.constructor);

}
util.inherits(AbstractMethodException, Error);

/**
 * @class
 * @param {number=} status
 * @constructor
 * @augments Error
 * @memberOf module:most-web.common
 */
function FileNotFoundException(message) {

    this.message = message || 'File not found';
}
util.inherits(FileNotFoundException, Error);
/**
 * @class
 * @constructor
 * @param {number=} status
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments Error
 * @memberOf module:most-web.common
 */
function HttpException(status, message, innerMessage) {
    var hstatus = (typeof status==='undefined' || status == null) ? 500 : parseInt(status);
    var err = errors.find(function(x) { return x.status === hstatus; });
    if (err) {
        this.title = err.title;
        this.message = message || err.message;
        this.status = err.status;
    }
    else {
        this.title = 'Internal Server Error';
        this.message = message || 'The server encountered an internal error and was unable to complete the request.';
        this.status = hstatus
    }
    this.innerMessage = innerMessage;
}

/**
 * @param {Error} err
 * @returns {Error}
 */
HttpException.create = function(err) {
    if (typeof err === 'undefined' || err==null)
        return new HttpException();
    else {
        if (err.status)
            return new HttpException(err.status, err.message);
        else
            return new HttpException(500, err.message);
    }
}

util.inherits(HttpException, Error);

/**
 * HTTP 400 Bad Request exception class
 * @class
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
function HttpBadRequest(message, innerMessage) {
    HttpBadRequest.super_.call(this, 400, message , innerMessage);

}
util.inherits(HttpBadRequest, HttpException);
/**
 * HTTP 404 Not Found Exception class
 * @class
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
 function HttpNotFoundException(message, innerMessage) {
    HttpNotFoundException.super_.call(this, 404, message, innerMessage);

}
util.inherits(HttpNotFoundException, HttpException);
/**
 * HTTP 405 Method Not Allowed exception class
 * @class
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
function HttpMethodNotAllowed(message, innerMessage) {
    HttpMethodNotAllowed.super_.call(this, 405, message, innerMessage);

}
util.inherits(HttpMethodNotAllowed, HttpException);
/**
 * HTTP 401 Unauthorized Exception class
 * @class
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
function HttpUnauthorizedException(message, innerMessage) {
    HttpUnauthorizedException.super_.call(this, 401, message, innerMessage);
}
util.inherits(HttpUnauthorizedException, HttpException);
/**
 * HTTP 403 Forbidden Exception class
 * @class
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
function HttpForbiddenException(message, innerMessage) {
    HttpForbiddenException.super_.call(this, 403, message, innerMessage);
}
util.inherits(HttpForbiddenException, HttpException);

/**
 * HTTP 500 Internal Server Error Exception class
 * @class HttpServerError
 * @param {string=} message
 * @param {string=} innerMessage
 * @augments HttpException
 * @memberOf module:most-web.common
 * */
function HttpServerError(message, innerMessage) {
    HttpServerError.super_.call(this, 500, message , innerMessage);
}
util.inherits(HttpServerError, HttpException);

/**
 * @type {RegExp}
 * @private
 */
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
/**
 * @param fn
 * @returns {*}
 * @private
 */
function getFunctionParams( fn ) {
    if (!isFunction(fn))
        return [];
    var fnStr = fn.toString().replace(STRIP_COMMENTS, '')
    var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)
    if(result === null)
        result = []
    return result
}
/**
 * @param fn {Function}
 * @returns {Boolean}
 * @private
 * */
function isFunction( fn ) {
    return typeof fn === 'function';
};

/**
 * @class UnknownValue
 * @constructor
 */
function UnknownValue() {
    //
}

UnknownValue.prototype.valueOf = function() { return null; }

UnknownValue.prototype.toJSON = function() { return null; }

UnknownValue.DateTimeRegex = /^(\d{4})(?:-?W(\d+)(?:-?(\d+)D?)?|(?:-(\d+))?-(\d+))(?:[T ](\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?)?(?:Z(-?\d*))?$/g;
UnknownValue.BooleanTrueRegex = /^true$/ig;
UnknownValue.BooleanFalseRegex = /^false$/ig;
UnknownValue.NullRegex = /^null$/ig;
UnknownValue.UndefinedRegex = /^undefined$/ig;
UnknownValue.IntegerRegex =/^[-+]?\d+$/g;
UnknownValue.FloatRegex =/^[+-]?\d+(\.\d+)?$/g;
/**
 * @class
 * @constructor
 */
function UnknownPropertyDescriptor(obj, name) {
    Object.defineProperty(this, 'value', { configurable:false, enumerable:true, get: function() { return obj[name]; }, set: function(value) { obj[name]=value; } });
    Object.defineProperty(this, 'name', { configurable:false, enumerable:true, get: function() { return name; } });
}
/**
 * @param {string} value
 */
UnknownValue.convert = function(value) {
    var result;
    if ((typeof value === 'string'))
    {
        if (value.length==0) {
            result = value
        }
        if (value.match(UnknownValue.BooleanTrueRegex)) {
            result = true;
        }
        else if (value.match(UnknownValue.BooleanFalseRegex)) {
            result = false;
        }
        else if (value.match(UnknownValue.NullRegex) || value.match(UnknownValue.UndefinedRegex)) {
            result = null;
        }
        else if (value.match(UnknownValue.IntegerRegex)) {
            result = parseInt(value);
        }
        else if (value.match(UnknownValue.FloatRegex)) {
            result = parseFloat(value);
        }
        else if (value.match(UnknownValue.DateTimeRegex)) {
            result = new Date(Date.parse(value));
        }
        else {
            result = value;
        }
    }
    else {
        result = value;
    }
    return result;
};

/**
 *
 * @param {*} origin
 * @param {string} expr
 * @param {string} value
 * @param {*=} options
 * @returns {*}
 */
UnknownValue.extend = function(origin, expr, value, options) {

    options = options || { convertValues:false };
    //find base notation
    var match = /(^\w+)\[/.exec(expr), name, descriptor, expr1;
    if (match) {
        //get property name
        name = match[1];
        //validate array property
        if (/^\d+$/g.test(name)) {
            //property is an array
            if (!util.isArray(origin.value))
                origin.value = [];
            // get new expression
            expr1 = expr.substr(match.index + match[1].length);
            UnknownValue.extend(origin, expr1, value);
        }
        else {
            //set property value (unknown)
            origin[name] = origin[name] || new UnknownValue();
            descriptor = new UnknownPropertyDescriptor(origin, name);
            // get new expression
            expr1 = expr.substr(match.index + match[1].length);
            UnknownValue.extend(descriptor, expr1, value);
        }
    }
    else if (expr.indexOf('[')==0) {
        //get property
        var re = /\[(.*?)\]/g;
        match = re.exec(expr);
        if (match) {
            name = match[1];
            // get new expression
            expr1 = expr.substr(match.index + match[0].length);
            if (/^\d+$/g.test(name)) {
                //property is an array
                if (!util.isArray(origin.value))
                    origin.value = [];
            }
            if (expr1.length==0) {
                if (origin.value instanceof UnknownValue) {
                    origin.value = {};
                }
                var typedValue;
                //convert string value
                if ((typeof value === 'string') && options.convertValues) {
                    typedValue = UnknownValue.convert(value);
                }
                else {
                    typedValue = value;
                }
                if (util.isArray(origin.value))
                    origin.value.push(typedValue);
                else
                    origin.value[name] = typedValue;
            }
            else {
                if (origin.value instanceof UnknownValue) {
                    origin.value = { };
                }
                origin.value[name] = origin.value[name] || new UnknownValue();
                descriptor = new UnknownPropertyDescriptor(origin.value, name);
                UnknownValue.extend(descriptor, expr1, value);
            }
        }
        else {
            throw new Error('Invalid object property notation. Expected [name]');
        }
    }
    else if (/^\w+$/.test(expr)) {
        if (options.convertValues)
            origin[expr] = UnknownValue.convert(value);
        else
            origin[expr] = value;
    }
    else {
        throw new Error('Invalid object property notation. Expected property[name] or [name]');
    }
    return origin;
};
/**
 * @type {Array}
 * @private
 */
var UUID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');

/**
 * @namespace
 * @memberOf module:most-web
 */
var common = {
    AbstractMethodException : AbstractMethodException,
    FileNotFoundException : FileNotFoundException,
    HttpException : HttpException,
    /**
     * @param {Error|*} err
     * @returns {HttpException}
     */
    httpError: function(err) {
        return HttpException.create(err);
    },
    HttpNotFoundException : HttpNotFoundException,
    HttpMethodNotAllowed : HttpMethodNotAllowed,
    HttpBadRequest: HttpBadRequest,
    HttpUnauthorizedException: HttpUnauthorizedException,
    HttpForbiddenException: HttpForbiddenException,
    HttpServerError:HttpServerError,
    /**
     * @returns {Array}
     * */
    getFunctionParams:getFunctionParams,
    /**
     * @param {function|*} fn
     * @returns {Boolean}
     * */
    isFunction:function(fn) {
        return isFunction(fn);
    },
    /**
     * Checks if the specified string argument is empty, undefined or null.
     * @param {string} s
     * @returns {boolean}
     */
    isEmptyString: function(s) {
        if (typeof s === 'undefined' || s===null)
            return true;
        if (typeof s === 'string') {
            return (s.replace(/^\s|\s$/ig,'').length === 0);
        }
        return true;
    },
    /**
     * Checks if the specified object argument is undefined or null.
     * @param {*} obj
     * @returns {boolean}
     */
    isNullOrUndefined: function(obj) {
        return (typeof obj === 'undefined' || obj === null);
    },
    /**
     * Checks if the specified object is an HttpException instance or inherits HttpException class.
     * @param {*} obj
     * @returns {boolean}
     */
    isHttpException: function(obj) {
        return (obj instanceof HttpException);
    },
    /**
     * Checks if the specified object argument is object.
     * @param {*} obj
     * @returns {boolean}
     */
    isObject: function(obj) {
        return !!(typeof obj === 'object' && obj !== null);
    },
    /**
     * Checks if the specified object argument is numeric or not.
     * @param {*} n
     * @returns {boolean}
     */
    isNumber: function(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
    },
    /**
     * Returns a random integer between a minimum and a maximum value
     * @param {number} min
     * @param {number} max
     */
    randomInt: function(min, max) {
        return Math.floor(Math.random()*max) + min;
    },
    /**
     * Returns a random string based on the length specified
     * @param {Number} length
     */
    randomChars: function(length) {
        length = length || 8;
        var chars = "abcdefghkmnopqursuvwxz2456789ABCDEFHJKLMNPQURSTUVWXYZ";
        var str = "";
        for(var i = 0; i < length; i++) {
            str += chars.substr(this.randomInt(0, chars.length-1),1);
        }
        return str;
    },
    /**
     * Converts a base-26 formatted string to the equivalent integer
     * @param {string} s A base-26 formatted string e.g. aaaaaaaa for 0, baaaaaaa for 1 etc
     * @return {number} The equivalent integer value
     */
    convertFromBase26 : function(s) {
        var num = 0;
        if (!/[a-z]{8}/.test(s)) {
            throw new Error('Invalid base-26 format.');
        }
        var a = 'a'.charCodeAt(0);
        for (var i = 7; i >=0; i--) {
            num = (num * 26) + (s[i].charCodeAt(0) - a);
        }
        return num;
    },
    /**
     * Converts an integer to the equivalent base-26 formatted string
     * @param {number} x The integer to be converted
     * @return {string} The equivalent string value
     */
    convertToBase26: function(x) {
        var num = parseInt(x);
        if (num<0) {
            throw new Error('A non-positive integer cannot be converted to base-26 format.');
        }
        if (num>208827064575) {
            throw new Error('A positive integer bigger than 208827064575 cannot be converted to base-26 format.');
        }
        var out = "", length= 1, a = 'a'.charCodeAt(0);
        while(length<=8)
        {
            out += String.fromCharCode(a + (num % 26))
            num = Math.floor(num / 26);
            length += 1;
        }
        return out;
    },
    /**
     * Returns a random string based on the length specified
     * @param {number} length
     */
    randomHex: function(length) {
        length = (length || 8)*2;
        var chars = "abcdef1234567890";
        var str = "";
        for(var i = 0; i < length; i++) {
            str += chars.substr(this.randomInt(0, chars.length-1),1);
        }
        return str;
    },
    /**
     * Returns a random GUID/UUID string
     */
    newGuid: function() {
        var chars = UUID_CHARS, uuid = [], i;
        // rfc4122, version 4 form
        var r;
        // rfc4122 requires these characters
        uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
        uuid[14] = '4';

        // Fill in random data.  At i==19 set the high bits of clock sequence as
        // per rfc4122, sec. 4.1.5
        for (i = 0; i < 36; i++) {
            if (!uuid[i]) {
                r = 0 | Math.random()*16;
                uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
            }
        }
        return uuid.join('');
    },
    /**
     * @param {IncomingMessage|ClientRequest} request
     * @returns {*}
     */
    parseCookies : function(request) {
        var list = {},
            rc = request.headers.cookie;
        rc && rc.split(';').forEach(function (cookie) {
            var parts = cookie.split('=');
            list[parts.shift().trim()] = unescape(parts.join('='));
        });
        return list;
    },
    /**
     *
     * @param {*} form
     * @returns {*}
     */
    parseForm : function(form) {
        var result = {};
        if (typeof form === 'undefined' || form==null)
            return result;
        var keys = Object.keys(form);
        keys.forEach(function(key) {
            if (form.hasOwnProperty(key))
            {
                UnknownValue.extend(result, key, form[key])
            }
        });
        return result;
    },
    /**
     * Parses any value or string and returns the resulted object.
     * @param {*} any
     * @returns {*}
     */
    parseValue: function(any) {
            return UnknownValue.convert(any);

    },
    /**
     * Parses any value and returns the equivalent integer.
     * @param {*} any
     * @returns {*}
     */
    parseInt: function(any) {
        return parseInt(any) || 0;
    },
    /**
     * Parses any value and returns the equivalent float number.
     * @param {*} any
     * @returns {*}
     */
    parseFloat: function(any) {
        return parseFloat(any) || 0;
    },
    /**
     * Parses any value and returns the equivalent boolean.
     * @param {*} any
     * @returns {*}
     */
    parseBoolean: function(any) {
        if (typeof any === 'undefined' || any == null)
            return false;
        else if (typeof any === 'number')
            return any != 0;
        else if (typeof any === 'string') {
            if (any.match(UnknownValue.IntegerRegex) || any.match(UnknownValue.FloatRegex)) {
                return parseInt(any, 10) != 0;
            }
            else if (any.match(UnknownValue.BooleanTrueRegex))
                return true;
            else if (any.match(UnknownValue.BooleanFalseRegex))
                return false;
            else if (/^yes$|^on$|^y$|^valid$/i.test(any))
                return true;
            else if (/^no$|^off$|^n$|^invalid$/i.test(any))
                return false;
            else
                return false;
        }
        else if (typeof any === 'boolean')
            return any;
        else {
            return (parseInt(any) || 0) != 0;
        }
    },
    /**
     *
     * @param {Error|string|{message:string,stack:string}|*} data
     */
    log:function(data) {
        if (data) {
            util.log(data);
            if (data.stack) {
                util.log(data.stack);
            }
        }
    },
    /**
     *
     * @param {Error|string|{message:string,stack:string}|*} data
     */
    debug:function(data) {
        if (process.env.NODE_ENV==='development')
            util.log(data);
    },
    /**
     * Validates the given parameter and returns true if this represents a relative url. Otherwise returns false.
     * @param {string} virtualPath
     * @return {boolean}
     */
    isRelativeUrl: function(virtualPath) {
        if (this.isNullOrUndefined(virtualPath))
            return false;
        if (typeof virtualPath !== 'string')
            throw new Error('Invalid virtualPath argument. Must be a string');
        if (/^(https?|file|ftps?|mailto|javascript|data:image\/[^;]{2,9};):/i.test(virtualPath))
            return false;
        if (virtualPath.indexOf('/')!=0)
            return !(virtualPath[0]=='\\');
        return true;
    },
    getBasicAuthHeader: function(username, password) {
        return "Basic " + (new Buffer(username +':'+password)).toString('base64');
    },
    md5 : function(value) {
        if (typeof value === 'undefined' || value == null) {
            return;
        }
        var md5 = crypto.createHash('md5');
        if (typeof value === 'string') {
            md5.update(value);
        }
        else if (value instanceof Date) {
            md5.update(value.toUTCString());
        }
        else {
            md5.update(JSON.stringify(value));
        }
        return md5.digest('hex');
    },
    sha1 : function(value) {
        if (typeof value === 'undefined' || value == null) {
            return;
        }
        var sha1 = crypto.createHash('sha1');
        if (typeof value === 'string') {
            sha1.update(value);
        }
        else if (value instanceof Date) {
            sha1.update(value.toUTCString());
        }
        else {
            sha1.update(JSON.stringify(value));
        }
        return sha1.digest('hex');
    }
};

if (typeof exports !== 'undefined') {
    module.exports = common;
}