/**
 * 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() {
  /**
   * 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 * */
  String.prototype.startsWith = function(prefix) {
    return this.indexOf(prefix) === 0;
  };

  String.prototype.endsWith = function(suffix) {
    return this.match(suffix+"$") == 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(false == false) {
      if(target.charAt(0) == '.') {
        result = current.endsWith(target);
      } else {
        result = current.endsWith('.' + target);
      }
    }
    
    return result;
  }

  /** determine if uri/url points to valid domain * */
  function isValidUrl(src) {
    var result = false;
    
    /** 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(!src.startsWith("//") && (src.charAt(0) == '/' || src.indexOf(':') == -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 = "/sinfo/" + 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 value = tokenValue;
    var action = form.getAttribute("action");
    
    if(action != null && isValidUrl(action)) {
      var uri = parseUri(action);
      value = pageTokens[uri] != null ? pageTokens[uri] : tokenValue;
    }
    
    var hidden = document.createElement("input");
    
    hidden.setAttribute("type", "hidden");
    hidden.setAttribute("name", tokenName);
    hidden.setAttribute("value", value);
    
    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)) {
      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 = false;
    
    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", "/sinfo/csrf/js", 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;
  }
  
  /**
   * 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, "www.mytimecloud.eu")) {
    /** 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", "/sinfo/csrf/js", false);
    xhr.setRequestHeader("FETCH-CSRF-TOKEN", "1");
    xhr.send(null);
    
    var token_pair = xhr.responseText;
    token_pair = token_pair.split(":");
    var token_name = token_pair[0];
    var token_value = token_pair[1];

      XMLHttpRequest.prototype.onsend = function(data) {
        if(isValidUrl(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);
    var msie = navigator.userAgent.match(/MSIE.\d/);
    if (msie)
    msie = msie[0].replace(/(MSIE| )/g, "");
    if ( msie && parseInt(msie) <= 8)
      addEvent(document, 'readystatechange', function() {
        if ( document.readyState === "complete" ) {
          injectTokens(token_name, token_value);
        }
      });
    else {
      addEvent(window,'DOMContentLoaded', function() {
        injectTokens(token_name, token_value);
      });
    }
  } else {
    alert("OWASP CSRFGuard JavaScript was included from within an unauthorized domain!");
  }
})();