/**
 * The OWASP CSRFGuard Project, BSD License
 * Eric Sheridan (eric@infraredsecurity.com), Copyright (c) 2011
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *    3. Neither the name of OWASP nor the names of its contributors may be used
 *       to endorse or promote products derived from this software without specific
 *       prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
(function() {
    if (window.csrfguarded) {
        return;
    }

    /**
     * Code to ensure our event always gets triggered when the DOM is updated.
     * @param obj
     * @param type
     * @param fn
     * @source http://www.dustindiaz.com/rock-solid-addevent/
     */
    function addEvent( obj, type, fn ) {
        if (obj.addEventListener) {
            obj.addEventListener( type, fn, false );
            EventCache.add(obj, type, fn);
        }
        else if (obj.attachEvent) {
            obj["e"+type+fn] = fn;
            obj[type+fn] = function() { obj["e"+type+fn]( window.event ); };
            obj.attachEvent( "on"+type, obj[type+fn] );
            EventCache.add(obj, type, fn);
        }
        else {
            obj["on"+type] = obj["e"+type+fn];
        }
    }

    var EventCache = function() {
        var listEvents = [];
        return {
            listEvents : listEvents,
            add : function(node, sEventName, fHandler) {
                listEvents.push(arguments);
            },
            flush : function() {
                var i, item;
                for (i = listEvents.length - 1; i >= 0; i = i - 1) {
                    item = listEvents[i];
                    if (item[0].removeEventListener) {
                        item[0].removeEventListener(item[1], item[2], item[3]);
                    }
                    if (item[1].substring(0, 2) != "on") {
                        item[1] = "on" + item[1];
                    }
                    if (item[0].detachEvent) {
                        item[0].detachEvent(item[1], item[2]);
                    }
                }
            }
        };
    }();

    /** string utility functions **/
    function startsWith(s, prefix) {
        return s.indexOf(prefix) === 0;
    }

    function endsWith(s, suffix) {
        return s.substring(s.length - suffix.length) === suffix;
    }

    /** hook using standards based prototype **/
    function hijackStandard() {
        XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
            this.url = url;

            this._open.apply(this, arguments);
        };

        XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function(data) {
            if (this.onsend != null) {
                this.onsend.apply(this, arguments);
            }

            this._send.apply(this, arguments);
        };
    }

    /** ie does not properly support prototype - wrap completely **/
    function hijackExplorer() {
        var _XMLHttpRequest = window.XMLHttpRequest;

        function alloc_XMLHttpRequest() {
            this.base = _XMLHttpRequest ? new _XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
        }

        function init_XMLHttpRequest() {
            return new alloc_XMLHttpRequest;
        }

        init_XMLHttpRequest.prototype = alloc_XMLHttpRequest.prototype;

        /** constants **/
        init_XMLHttpRequest.UNSENT = 0;
        init_XMLHttpRequest.OPENED = 1;
        init_XMLHttpRequest.HEADERS_RECEIVED = 2;
        init_XMLHttpRequest.LOADING = 3;
        init_XMLHttpRequest.DONE = 4;

        /** properties **/
        init_XMLHttpRequest.prototype.status = 0;
        init_XMLHttpRequest.prototype.statusText = "";
        init_XMLHttpRequest.prototype.readyState = init_XMLHttpRequest.UNSENT;
        init_XMLHttpRequest.prototype.responseText = "";
        init_XMLHttpRequest.prototype.responseXML = null;
        init_XMLHttpRequest.prototype.onsend = null;

        init_XMLHttpRequest.url = null;
        init_XMLHttpRequest.onreadystatechange = null;

        /** methods **/
        init_XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
            var self = this;
            this.url = url;

            this.base.onreadystatechange = function() {
                try { self.status = self.base.status; } catch (e) { }
                try { self.statusText = self.base.statusText; } catch (e) { }
                try { self.readyState = self.base.readyState; } catch (e) { }
                try { self.responseText = self.base.responseText; } catch(e) { }
                try { self.responseXML = self.base.responseXML; } catch(e) { }

                if (self.onreadystatechange != null) {
                    self.onreadystatechange.apply(this, arguments);
                }
            };

            this.base.open(method, url, async, user, pass);
        };

        init_XMLHttpRequest.prototype.send = function(data) {
            if (this.onsend != null) {
                this.onsend.apply(this, arguments);
            }

            this.base.send(data);
        };

        init_XMLHttpRequest.prototype.abort = function() {
            this.base.abort();
        };

        init_XMLHttpRequest.prototype.getAllResponseHeaders = function() {
            return this.base.getAllResponseHeaders();
        };

        init_XMLHttpRequest.prototype.getResponseHeader = function(name) {
            return this.base.getResponseHeader(name);
        };

        init_XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
            return this.base.setRequestHeader(name, value);
        };

        /** hook **/
        window.XMLHttpRequest = init_XMLHttpRequest;
    }

    /** check if valid domain based on domainStrict **/
    function isValidDomain(current, target) {
        var result = false;

        /** check exact or subdomain match **/
        if (current == target) {
            result = true;
        } else if (true == false) {
            if (target.charAt(0) == '.') {
                result = endsWith(current, target);
            } else {
                result = endsWith(current, '.' + target);
            }
        }

        return result;
    }

    /** determine if uri/url points to valid domain **/
    function isValidUrl(src) {
        var result = false;
        var urlStartsWithProtocol = /^[a-zA-Z][a-zA-Z0-9.+-]*:/;

        /** parse out domain to make sure it points to our own **/
        if (src.substring(0, 7) == "http://" || src.substring(0, 8) == "https://") {
            var token = "://";
            var index = src.indexOf(token);
            var part = src.substring(index + token.length);
            var domain = "";

            /** parse up to end, first slash, or anchor **/
            for (var i=0; i<part.length; i++) {
                var character = part.charAt(i);

                if (character == '/' || character == ':' || character == '#') {
                    break;
                } else {
                    domain += character;
                }
            }

            result = isValidDomain(document.domain, domain);
            /** explicitly skip anchors **/
        } else if (src.charAt(0) == '#') {
            result = false;
            /** ensure it is a local resource without a protocol **/
        } else if (!startsWith(src, "//") && (src.charAt(0) == '/' || src.search(urlStartsWithProtocol) === -1)) {
            result = true;
        }

        return result;
    }

    /** parse uri from url **/
    function parseUri(url) {
        var uri = "";
        var token = "://";
        var index = url.indexOf(token);
        var part = "";

        /**
         * ensure to skip protocol and prepend context path for non-qualified
         * resources (ex: "protect.html" vs
         * "/Owasp.CsrfGuard.Test/protect.html").
         */
        if (index > 0) {
            part = url.substring(index + token.length);
        } else if (url.charAt(0) != '/') {
            part = "/modules/" + url;
        } else {
            part = url;
        }

        /** parse up to end or query string **/
        var uriContext = (index == -1);

        for (var i=0; i<part.length; i++) {
            var character = part.charAt(i);

            if (character == '/') {
                uriContext = true;
            } else if (uriContext == true && (character == '?' || character == '#')) {
                uriContext = false;
                break;
            }

            if (uriContext == true) {
                uri += character;
            }
        }

        return uri;
    }

    /** inject tokens as hidden fields into forms **/
    function injectTokenForm(form, tokenName, tokenValue, pageTokens,injectGetForms) {

        if (!injectGetForms) {
            var method = form.getAttribute("method");

            if ((typeof method != 'undefined') && method != null && method.toLowerCase() == "get") {
                return;
            }
        }

        var action = form.getAttribute("action");

        if (action != null && isValidUrl(action) && isDotDoUrl(action)) {
            var uri = parseUri(action);
            var hidden = document.createElement("input");

            hidden.setAttribute("type", "hidden");
            hidden.setAttribute("name", tokenName);
            hidden.setAttribute("value", (pageTokens[uri] != null ? pageTokens[uri] : tokenValue));

            form.appendChild(hidden);
        }
    }

    /** inject tokens as query string parameters into url **/
    function injectTokenAttribute(element, attr, tokenName, tokenValue, pageTokens) {
        var location = element.getAttribute(attr);

        if (location != null && isValidUrl(location) && isDotDoUrl(location)) {
            var uri = parseUri(location);
            var value = (pageTokens[uri] != null ? pageTokens[uri] : tokenValue);

            if (location.indexOf('?') != -1) {
                location = location + '&' + tokenName + '=' + value;
            } else {
                location = location + '?' + tokenName + '=' + value;
            }

            try {
                element.setAttribute(attr, location);
            } catch (e) {
                // attempted to set/update unsupported attribute
            }
        }
    }

    /** inject csrf prevention tokens throughout dom **/
    function injectTokens(tokenName, tokenValue) {
        /** obtain reference to page tokens if enabled **/
        var pageTokens = {};

        if (false == true) {
            pageTokens = requestPageTokens();
        }

        /** iterate over all elements and injection token **/
        var all = document.all ? document.all : document.getElementsByTagName('*');
        var len = all.length;

        //these are read from the csrf guard config file(s)
        var injectForms = true;
        var injectGetForms = true;
        var injectFormAttributes = false;
        var injectAttributes = true;

        for (var i=0; i<len; i++) {
            var element = all[i];

            /** inject into form **/
            if (element.tagName.toLowerCase() == "form") {
                if (injectForms) {
                    injectTokenForm(element, tokenName, tokenValue, pageTokens,injectGetForms);

                    /** adjust array length after addition of new element **/
                    len = all.length;
                }
                if (injectFormAttributes) {
                    injectTokenAttribute(element, "action", tokenName, tokenValue, pageTokens);
                }
                /** inject into attribute **/
            } else if (injectAttributes) {
                injectTokenAttribute(element, "src", tokenName, tokenValue, pageTokens);
                injectTokenAttribute(element, "href", tokenName, tokenValue, pageTokens);
            }
        }
    }

    /** obtain array of page specific tokens **/
    function requestPageTokens() {
        var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
        var pageTokens = {};

        xhr.open("POST", "/modules/CsrfServlet", false);
        xhr.send(null);

        var text = xhr.responseText;
        var name = "";
        var value = "";
        var nameContext = true;

        for (var i=0; i<text.length; i++) {
            var character = text.charAt(i);

            if (character == ':') {
                nameContext = false;
            } else if (character != ',') {
                if (nameContext == true) {
                    name += character;
                } else {
                    value += character;
                }
            }

            if (character == ',' || (i + 1) >= text.length) {
                pageTokens[name] = value;
                name = "";
                value = "";
                nameContext = true;
            }
        }

        return pageTokens;
    }

    function isDotDoUrl(url) {
        let pathPart = (url.indexOf('?') !== -1) ? url.substring(0, url.indexOf('?')) : url;
        return endsWith(pathPart,'.do') ||  pathPart.indexOf('/*') > -1;
    }

    /**
     * Only inject the tokens if the JavaScript was referenced from HTML that
     * was served by us. Otherwise, the code was referenced from malicious HTML
     * which may be trying to steal tokens using JavaScript hijacking techniques.
     * The token is now removed and fetched using another POST request to solve,
     * the token hijacking problem.
     */
    if (isValidDomain(document.domain, "casco.iso.org")) {
        var token_name = 'CSRFTOKEN';
        var token_value = '';
        /** optionally include Ajax support **/
        if (true == true) {
            if (navigator.appName == "Microsoft Internet Explorer") {
                hijackExplorer();
            } else {
                hijackStandard();
            }

            var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
            var csrfToken = {};
            xhr.open("POST", "/modules/CsrfServlet", false);
            xhr.setRequestHeader("FETCH-CSRF-TOKEN", "1");
            xhr.send(null);

            var token_pair = xhr.responseText;
            token_pair = token_pair.split(":");
            token_name = token_pair[0];
            token_value = token_pair[1];

            XMLHttpRequest.prototype.onsend = function(data) {
                if (isValidUrl(this.url) && isDotDoUrl(this.url)) {
                    this.setRequestHeader("X-Requested-With", "XMLHttpRequest");
                    this.setRequestHeader(token_name, token_value);
                }
            };
        }

        /** update nodes in DOM after load **/
        addEvent(window,'unload',EventCache.flush);
        addEvent(window,'DOMContentLoaded', function() {
            injectTokens(token_name, token_value);
        });

        window.csrfguarded = true;
    } else {
        alert("OWASP CSRFGuard JavaScript was included from within an unauthorized domain!");
    }
})();
