LivePersonInc/chronosjs

View on GitHub
src/courier/PostMessageUtilities.js

Summary

Maintainability
A
3 hrs
Test Coverage
;(function (root, chronosRoot, factory) {
    "use strict";

    /* istanbul ignore if  */
    //<amd>
    if ("function" === typeof define && define.amd) {

        // AMD. Register as an anonymous module.
        define("Chronos.PostMessageUtilities", [], function () {
            return factory(root, chronosRoot, true);
        });
        return;
    }
    //</amd>
    /* istanbul ignore next  */
    if ("object" !== typeof exports) {
        chronosRoot.Chronos = chronosRoot.Chronos || {};
        factory(root, chronosRoot.Chronos);
    }
}(this, typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, hide) {
    "use strict";

    var SEQUENCE_FORMAT = "_xxxxxx-4xxx-yxxx";

    /**
     * This function was added because of incompatibility between the JSON.stringify and Prototype.js library
     * When a customer uses Prototype.js library, It overrides the Array.prototype.toJSON function of the native JSON
     * uses. This causes arrays to be double quoted and Shark to fail on those SDEs.
     * The function accepts a value and uses the native JSON.stringify
     * Can throw an exception (same as JSON.stringify).
     * @returns {String} the strigified object
     */
    function stringify() {
        var stringified;
        var toJSONPrototype;

        if ("function" === typeof Array.prototype.toJSON) {
            toJSONPrototype = Array.prototype.toJSON;
            Array.prototype.toJSON = void 0;

            try {
                stringified = JSON.stringify.apply(null, arguments);
            }
            catch (ex) {
                /* istanbul ignore next  */
                Array.prototype.toJSON = toJSONPrototype;
                /* istanbul ignore next  */
                throw ex;
            }

            Array.prototype.toJSON = toJSONPrototype;
        }
        else {
            stringified = JSON.stringify.apply(null, arguments);
        }

        return stringified;
    }

    /**
     * Method to identify whether the browser supports passing object references to postMessage API
     * @returns {Boolean} whether the browser supports passing object references to postMessage API
     */
    function hasPostMessageObjectsSupport() {
        var hasObjectsSupport = true;
        try {
            root.postMessage({
                toString:function() {
                    hasObjectsSupport = false;
                }
            }, "*");
        }
        catch (ex) {
            // Browsers which has postMessage Objects support sends messages using
            // the structured clone algorithm - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
            // In which Error and Function objects cannot be duplicated by the structured clone algorithm; attempting to do so will throw a DATA_CLONE_ERR exception.
            if (ex && 'DataCloneError' !== ex.name) {
                hasObjectsSupport = false;
            }
        }

        return hasObjectsSupport;
    }


    /**
     * Method to create a unique sequence
     * @param {String} format - the format for the unique name eg. xxxxx-xx4xxx-yxxxx
     * @returns {String} the unique iFrame name
     */
    function createUniqueSequence(format) {
        return format && format.replace(/[xy]/g, function(chr) {
                var rnd = Math.random() * 16 | 0;
                var val = chr === "x" ? rnd : (rnd & 0x3 | 0x8);

                return val.toString(16);
            });
    }

    /**
     * Method to validate and parse an input number
     * @param {Number} input - the input value to parse
     * @param {Number} defaultValue - the default value to return in case of invalid input
     * @returns {Number} the number to return
     */
    function parseNumber(input, defaultValue) {
        return !isNaN(input) && 0 < input ? parseInt(input, 10) : defaultValue;
    }

    /**
     * Method to validate and parse a function reference
     * @param {Function} input - the input value to parse
     * @param {Function|Boolean} defaultValue - the default value to return in case of invalid input or true for empty function
     * @returns {Function} the function to return
     */
    function parseFunction(input, defaultValue) {
        return (("function" === typeof input) ? input : (true === defaultValue ? function() {} : defaultValue));
    }

    /**
     * Function to extract the host domain from any URL
     * @param {String} url - the url to resolve the host for
     * @param {Object} [win] - the window to resolve the host for
     * @param {Boolean} [top] - boolean indication for using helper of the top window if needed
     * @returns {String} the host
     */
    function getHost(url, win, top) {
        var domainRegEx = new RegExp(/(http{1}s{0,1}?:\/\/)([^\/\?]+)(\/?)/ig);
        var matches;
        var domain;
        var frame;

        if (url && 0 === url.indexOf("http")) {
            matches = domainRegEx.exec(url);
        }
        else { // This is a partial url so we assume it's relative, this is mainly nice for tests
            frame = top ? (win.top || (win.contentWindow && win.contentWindow.parent) || window) : win;
            return frame.location.protocol + "//" + frame.location.host;
        }

        if (matches && 3 <= matches.length && "" !== matches[2]) {
            domain = matches[1].toLowerCase() + matches[2].toLowerCase(); // 0 - full match 1- HTTPS 2- domain
        }

        return domain;
    }

    /**
     * Method to resolve the needed origin parameters from url
     * @param {String} [hostParam] - string to represent the name of the host parameter in querystring
     * @param {String} [url] - string to represent the url to resolve parameters from
     * @returns {String} the parameter from the url
     */
    function resolveParameters(hostParam, url) {
        var param;
        var value = getURLParameter("lpHost", url);

        if (!value) {
            param = getURLParameter("hostParam", url) || hostParam;

            if (param) {
                value = getURLParameter(param, url);
            }
        }

        return value;
    }

    /**
     * Method to resolve the needed origin
     * @param {Object} [target] - the target to resolve the host for
     * @param {Boolean} [top] - boolean indication for using helper of the top window if needed
     * @param {String} [hostParam] - string to represent the name of the host parameter in querystring
     * @returns {String} the origin for the target
     */
    function resolveOrigin(target, top, hostParam) {
        var origin;
        var url;
        var ref;

        try {
            url = target && target.contentWindow && "undefined" !== typeof Window && !(target instanceof Window) && target.getAttribute && target.getAttribute("src");
        }
        catch(ex) {}

        try {
            if (!url) {
                url = resolveParameters(hostParam);
            }

            if (!url) {
                url = document.referrer;
                ref = true;
            }

            if (url) {
                url = decodeURIComponent(url);

                if (ref) {
                    url = resolveParameters(hostParam, url);
                }
            }

            origin = getHost(url, target, top);
        }
        catch(ex) {
            log("Cannot parse origin", "ERROR", "PostMessageUtilities");
        }

        return origin || "*";
    }

    /**
     * Method to retrieve a url parameter from querystring by name
     * @param {String} name - the name of the parameter
     * @param {String} [url] - optional url to parse
     * @returns {String} the url parameter value
     */
    function getURLParameter(name, url) {
        return decodeURIComponent((new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(url || document.location.search) || [void 0, ""])[1].replace(/\+/g, "%20")) || null;
    }

    /**
     * Method to delay a message execution (async)
     * @param {Function} method - the function to delay
     * @param {Number} [milliseconds] - optional milliseconds to delay or false to run immediately
     */
    function delay(method, milliseconds) {
        var timer;
        /* istanbul ignore if  */
        if ("undefined" !== typeof setImmediate && (isNaN(milliseconds) || 0 >= milliseconds)) {
            timer = setImmediate(method);
        }
        else if (false === milliseconds) {
            method();
        }
        else {
            timer = setTimeout(method, (isNaN(milliseconds) || 0 >= milliseconds) ? 0 : parseInt(milliseconds, 10));
        }

        return function() {
            clearDelay(timer);
        };
    }

    /**
     * Method to clear the delay of a message execution (async)
     * @param {Number} id - the id of the timer to clear
     */
    function clearDelay(timer) {
        var timerId = parseNumber(timer);
        if (timerId) {
            /* istanbul ignore if  */
            if ("undefined" !== typeof clearImmediate) {
                clearImmediate(timerId);
            }
            else {
                clearTimeout(timerId);
            }
        }
    }

    /**
     * Method to add DOM events listener to an element
     * @param {Object} element - the element we're binding to
     * @param {String} event - the event we want to bind
     * @param {Function} callback - the function to execute
     */
    function addEventListener(element, event, callback) {
        /* istanbul ignore else: IE9- only  */
        if (element.addEventListener) {
            element.addEventListener(event, callback, false);
        }
        else {
            element.attachEvent("on" + event, callback);
        }

        return function() {
            removeEventListener(element, event, callback);
        };
    }

    /**
     * Method to add DOM events listener to an element
     * @param {Object} element - the element we're binding to
     * @param {String} event - the event we want to bind
     * @param {Function} callback - the function to execute
     */
    function removeEventListener(element, event, callback) {
        /* istanbul ignore else: IE9- only  */
        if (element.removeEventListener) {
            element.removeEventListener(event, callback, false);
        }
        else {
            element.detachEvent("on" + event, callback);
        }
    }

    /**
     * Method to implement a simple logging based on lptag
     * @param {String} msg - the message to log
     * @param {String} level - the logging level of the message
     * @param {String} app - the app which logs
     */
    /* istanbul ignore next  */
    function log(msg, level, app) {
        if (root && "function" === typeof root.log) {
            root.log(msg, level, app);
        }
    }

    /**
     * Method to polyfill bind native functionality in case it does not exist
     * Based on implementation from:
     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
     * @param {Object} object - the object to bind to
     * @returns {Function} the bound function
     */
    /* istanbul ignore next */
    function bind(object) {
        /*jshint validthis:true */
        var args;
        var fn;

        if ("function" !== typeof this) {
            // Closest thing possible to the ECMAScript 5
            // Internal IsCallable function
            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
        }

        args = Array.prototype.slice.call(arguments, 1);
        fn = this;

        function Empty() {}

        function bound() {
            return fn.apply(this instanceof Empty && object ? this : object,
                args.concat(Array.prototype.slice.call(arguments)));
        }

        Empty.prototype = this.prototype;
        bound.prototype = new Empty();

        return bound;
    }

    /* istanbul ignore if  */
    if (!Function.prototype.bind) {
        Function.prototype.bind = bind;
    }

    // attach properties to the exports object to define
    // the exported module properties.
    var ret = {
        SEQUENCE_FORMAT: SEQUENCE_FORMAT,
        stringify: stringify,
        hasPostMessageObjectsSupport: hasPostMessageObjectsSupport,
        createUniqueSequence: createUniqueSequence,
        parseNumber: parseNumber,
        parseFunction: parseFunction,
        getHost: getHost,
        resolveOrigin: resolveOrigin,
        getURLParameter: getURLParameter,
        delay: delay,
        addEventListener: addEventListener,
        removeEventListener: removeEventListener,
        log: log,
        bind: bind
    };
    if (!hide) {
        exports.PostMessageUtilities = exports.PostMessageUtilities || ret;
    }
    return ret;
}));