Home Reference Source

wombat_node_module_style/wombat.js

const WombatInit = require('./wombatInit');
const WombatLocation = require('./wombatLocation')
const {
  equals_any,
  starts_with,
  ends_with,
  def_prop,
  get_orig_getter,
  get_orig_setter,
  next_parent
} = require('./utils');

/**
 * The Wombat proper class
 */
class Wombat extends WombatInit {
  /**
   * @param {Window} $wbwindow
   * @param {Object} wbinfo
   */
  constructor ($wbwindow, wbinfo) {
    super($wbwindow, wbinfo);
    this.update_location = this.update_location.bind(this);
    this.check_location_change = this.check_location_change.bind(this);
    this.check_all_locations = this.check_all_locations.bind(this);
  }

  /**
   * @desc Override HTMLBaseElement.{getAttribute,href} and Node.baseURI
   */
  override_htmlBaseElem_nodeBaseURI () {
    if (!Object.defineProperty) {
      return;
    }

    // <base> element .getAttribute()
    var orig_getAttribute = this.$wbwindow.HTMLBaseElement.prototype.getAttribute;
    var Wombat = this;
    this.$wbwindow.HTMLBaseElement.prototype.getAttribute = function (name) {
      var result = orig_getAttribute.call(this, name);
      if (name === 'href') {
        result = Wombat.extract_orig(result);
      }
      return result;
    };

    // <base> element .href
    var base_href_get = function () {
      return this.getAttribute('href');
    };

    def_prop(this.$wbwindow.HTMLBaseElement.prototype, 'href', undefined, base_href_get);

    // Shared baseURI
    this.override_prop_extract(this.$wbwindow.Node.prototype, 'baseURI');
  }

  /**
   * @desc Override XMLHttpRequest.prototype.open in order to
   * - rewrite the URL unless Wombat is to not perform rewrites.
   * - if the URL begins with "data:" add X-Pywb-Requested-With
   */
  override_ajax () {
    if (!this.$wbwindow.XMLHttpRequest ||
      !this.$wbwindow.XMLHttpRequest.prototype ||
      !this.$wbwindow.XMLHttpRequest.prototype.open) {
      return;
    }

    var orig = this.$wbwindow.XMLHttpRequest.prototype.open;
    var Wombat = this;

    function open_rewritten (method, url, async, user, password) {
      if (!this._no_rewrite) {
        url = Wombat.rewrite_url(url);
      }

      // defaults to true
      if (async !== false) {
        async = true;
      }

      var result = orig.call(this, method, url, async, user, password);
      if (!starts_with(url, 'data:')) {
        this.setRequestHeader('X-Pywb-Requested-With', 'XMLHttpRequest');
      }
    }

    this.$wbwindow.XMLHttpRequest.prototype.open = open_rewritten;

    // responseURL override
    this.override_prop_extract(this.$wbwindow.XMLHttpRequest.prototype, 'responseURL');
  }

  /**
   * @desc Override document.{write,writeln,open}
   * - rewrites the html string
   * - if the html string is an html element that is contains its own window instance
   *   initialize a new instance of Wombat associated with the new window
   */
  override_document_write () {
    if (!this.$wbwindow.DOMParser) {
      return;
    }

    // Write
    var orig_doc_write = this.$wbwindow.document.write;
    var Wombat = this;
    var new_write = function (string) {
      var new_buff = Wombat.rewrite_html(string, true);
      if (!new_buff) {
        return;
      }
      var res = orig_doc_write.call(this, new_buff);
      Wombat.init_new_window_wombat(this.defaultView);
      return res;
    };

    this.$wbwindow.document.write = new_write;
    this.$wbwindow.Document.prototype.write = new_write;

    // Writeln
    var orig_doc_writeln = this.$wbwindow.document.writeln;

    var new_writeln = function (string) {
      var new_buff = Wombat.rewrite_html(string, true);
      if (!new_buff) {
        return;
      }
      var res = orig_doc_writeln.call(this, new_buff);
      Wombat.init_new_window_wombat(this.defaultView);
      return res;
    };

    this.$wbwindow.document.writeln = new_writeln;
    this.$wbwindow.Document.prototype.writeln = new_writeln;

    // Open
    var orig_doc_open = this.$wbwindow.document.open;

    var new_open = function () {
      var res = orig_doc_open.call(this);
      Wombat.init_new_window_wombat(this.defaultView);
      return res;
    };

    this.$wbwindow.document.open = new_open;
    this.$wbwindow.Document.prototype.open = new_open;
  }

  /**
   * @desc Overrides a function on the history object by name
   * @param {string} func_name the name of the history function to override
   * @return {function} the rewritten func
   */
  override_history_func (func_name) {
    if (!this.$wbwindow.history) {
      return;
    }

    var orig_func = this.$wbwindow.history[func_name];

    if (!orig_func) {
      return;
    }

    this.$wbwindow.history['_orig_' + func_name] = orig_func;
    var me = this;

    function rewritten_func (state_obj, title, url) {
      url = me.rewrite_url(url);

      var abs_url = me.extract_orig(url);

      if (!abs_url) {
        abs_url = me.$wbwindow.WB_wombat_location.href;
      }

      if (abs_url &&
        (abs_url !== me.$wbwindow.WB_wombat_location.origin && me.$wbwindow.WB_wombat_location.href !== 'about:blank') &&
        !starts_with(abs_url, me.$wbwindow.WB_wombat_location.origin + '/')) {
        throw new DOMException('Invalid history change: ' + abs_url);
      }

      if (url === me.$wbwindow.location.href) {
        return;
      }

      orig_func.call(this, state_obj, title, url);

      if (me.$wbwindow.__WB_top_frame) {
        var message = {
          'url': abs_url,
          'ts': me.wb_info.timestamp,
          'request_ts': me.wb_info.request_ts,
          'is_live': me.wb_info.is_live,
          'title': title,
          'wb_type': func_name
        };

        me.$wbwindow.__WB_top_frame.postMessage(message, me.wb_info.top_host);
      }
    }

    this.$wbwindow.history[func_name] = rewritten_func;
    if (this.$wbwindow.History && this.$wbwindow.History.prototype) {
      this.$wbwindow.History.prototype[func_name] = rewritten_func;
    }

    return rewritten_func;
  }

  /**
   * @desc Overrides a function on the history object by name
   * @param {string} func_name
   * @return {function} the rewritten func
   */
  override_history_nav (func_name) {
    if (!this.$wbwindow.history) {
      return;
    }

    // Only useful for framed replay
    if (!this.$wbwindow.__WB_top_frame) {
      return;
    }

    var orig_func = this.$wbwindow.history[func_name];

    if (!orig_func) {
      return;
    }

    var me = this;

    function rewritten_func () {
      orig_func.apply(this, arguments);

      var message = {
        'wb_type': func_name
      };

      if (func_name === 'go') {
        message['param'] = arguments[0];
      }

      if (me.$wbwindow.__WB_top_frame) {
        me.$wbwindow.__WB_top_frame.postMessage(message, me.wb_info.top_host);
      }
    }

    this.$wbwindow.history[func_name] = rewritten_func;
    if (this.$wbwindow.History && this.$wbwindow.History.prototype) {
      this.$wbwindow.History.prototype[func_name] = rewritten_func;
    }

    return rewritten_func;
  }

  /**
   *
   * @param {Object.prototype} proto
   * @param {string} prop the prop name to define a getter for
   * @param {?function} cond function that determines if the returned value from the getter
   * is the result of {@link WombatRewriter#extract_orig} on the original value
   */
  override_prop_extract (proto, prop, cond) {
    var orig_getter = get_orig_getter(proto, prop);
    var Wombat = this;
    if (orig_getter) {
      var new_getter = function () {
        var res = Wombat.orig_getter.call(this);
        if (!cond || cond(this)) {
          res = Wombat.extract_orig(res);
        }
        return res;
      };

      def_prop(proto, prop, undefined, new_getter);
    }
  }

  /**
   * @desc Override window.open in order to
   * - rewrite the url of the new window
   * - initialize and associate a new instance of Wombat for the new window
   * - override the open method of the current replay windows frames
   * @todo was init_open_override
   */
  override_window_open () {
    var orig = this.$wbwindow.open;

    if (this.$wbwindow.Window.prototype.open) {
      orig = this.$wbwindow.Window.prototype.open;
    }
    var Wombat = this;
    var open_rewritten = function (strUrl, strWindowName, strWindowFeatures) {
      strUrl = Wombat.rewrite_url(strUrl, false, '');
      var res = orig.call(this, strUrl, strWindowName, strWindowFeatures);
      Wombat.init_new_window_wombat(res, strUrl);
      return res;
    };

    this.$wbwindow.open = open_rewritten;

    if (this.$wbwindow.Window.prototype.open) {
      this.$wbwindow.Window.prototype.open = open_rewritten;
    }

    for (var i = 0; i < this.$wbwindow.frames.length; i++) {
      try {
        this.$wbwindow.frames[i].open = open_rewritten;
      } catch (e) {
        console.log(e);
      }
    }
  }

  /**
   * @desc Override document.createElement.
   * - if the element to be created is an HTML.Form override its action attribute.
   * - if the skip parameter of document.createElement is set to a truthy value
   *   adds _no_rewrite property to the element indicating it is not to be rewritten
   */
  override_createElement () {
    if (!this.$wbwindow.document.createElement ||
      !this.$wbwindow.Document.prototype.createElement) {
      return;
    }

    var orig_createElement = this.$wbwindow.document.createElement;
    var Wombat = this;
    var createElement_override = function (tagName, skip) {
      var created = orig_createElement.call(this, tagName);
      if (!created) {
        return created;
      }
      if (skip) {
        created._no_rewrite = true;
      } else {
        // form override
        if (created.tagName === 'FORM') {
          Wombat.override_attr(created, 'action', '', true);
        }
      }

      return created;
    };

    this.$wbwindow.Document.prototype.createElement = createElement_override;
    this.$wbwindow.document.createElement = createElement_override;
  }

  /**
   * @desc Override the getter/setter of an objects property
   * @param {Object} obj the object to apply the override
   * @param {string} attr the name of the property
   * @param {string} mod
   * @param {boolean} default_to_setget
   */
  override_attr (obj, attr, mod, default_to_setget) {
    var orig_getter = get_orig_getter(obj, attr);
    var orig_setter = get_orig_setter(obj, attr);
    var Wombat = this;

    var setter = function (orig) {
      var val;

      if (mod === 'cs_' && orig.indexOf('data:text/css') === 0) {
        var decoded;

        try {
          decoded = decodeURIComponent(orig);
        } catch (e) {
          decoded = orig;
        }

        if (decoded !== orig) {
          val = Wombat.rewrite_style(decoded);
          var parts = val.split(',', 2);
          val = parts[0] + ',' + encodeURIComponent(parts[1]);
        } else {
          val = Wombat.rewrite_style(orig);
        }
      } else {
        val = Wombat.rewrite_url(orig, false, mod);
      }

      if (orig_setter) {
        return orig_setter.call(this, val);
      } else if (default_to_setget) {
        return Wombat.wb_setAttribute.call(this, attr, val);
      }
    };

    var getter = function () {
      var res;

      if (orig_getter) {
        res = orig_getter.call(this);
      } else if (default_to_setget) {
        res = Wombat.wb_getAttribute.call(this, attr);
      }
      res = Wombat.extract_orig(res);

      return res;
    };

    def_prop(obj, attr, setter, getter);
  }

  /**
   * @desc Override Attr.{nodeValue,value}
   */
  override_attr_props () {
    var Wombat = this;

    function is_rw_attr (attr) {
      if (attr && equals_any(attr.nodeName, Wombat.REWRITE_ATTRS)) {
        return true;
      }
      return false;
    }

    this.override_prop_extract(this.$wbwindow.Attr.prototype, 'nodeValue', is_rw_attr);
    this.override_prop_extract(this.$wbwindow.Attr.prototype, 'value', is_rw_attr);
  }

  /**
   * @desc Override the a style attribute on an object
   * @param {Object} obj the style prototype to apply the override to
   * @param {string} attr the name of the property to override
   * @param {string} prop_name the name of property to get/set
   */
  override_style_attr (obj, attr, prop_name) {
    var orig_getter = get_orig_getter(obj, attr);
    var orig_setter = get_orig_setter(obj, attr);
    var Wombat = this;

    var setter = function (orig) {
      var val = Wombat.rewrite_style(orig);
      if (orig_setter) {
        orig_setter.call(this, val);
      } else {
        this.setProperty(prop_name, val);
      }

      return val;
    };

    var getter = function () {
      if (orig_getter) {
        return orig_getter.call(this);
      } else {
        return this.getPropertyValue(prop_name);
      }
    };

    if ((orig_setter && orig_getter) || prop_name) {
      def_prop(obj, attr, setter, getter);
    }
  }

  /**
   * @desc
   * - Override the href, src attributes of HTML Elements
   *    - ``HTMLLinkElement.prototype, href, cs_``
   *    - ``CSSStyleSheet.prototype, href, cs_``
   *    - ``HTMLImageElement.prototype, src, im_``
   *    - ``HTMLIFrameElement.prototype, src, if_``
   *    - ``HTMLScriptElement.prototype, src, js_``
   *    - ``HTMLVideoElement.prototype, src, oe_``
   *    - ``HTMLVideoElement.prototype, poster, im_``
   *    - ``HTMLAudioElement.prototype, src, oe_``
   *    - ``HTMLAudioElement.prototype, poster, im_``
   *    - ``HTMLSourceElement.prototype, src, oe_``
   *    - ``HTMLSourceElement.prototype, srcset, oe_``
   *    - ``HTMLInputElement.prototype, src, oe_``
   *    - ``HTMLEmbedElement.prototype, src, oe_``
   *    - ``HTMLObjectElement.prototype, data, oe_``
   * - Override the attribute of CSSStyleDeclaration.prototype if not FireFox otherwise  CSS2Properties.prototype
   *    - ``cssText``
   *    - ``background, background``
   *    - ``backgroundImage, background-image``
   *    - ``listStyle, list-style``
   *    - ``listStyleImage, list-style-image``
   *    - ``border, border``
   *    - ``borderImage, border-image``
   *    - ``borderImageSource, border-image-source``
   */
  attr_overrides () {
    this.override_attr(this.$wbwindow.HTMLLinkElement.prototype, 'href', 'cs_');
    this.override_attr(this.$wbwindow.CSSStyleSheet.prototype, 'href', 'cs_');
    this.override_attr(this.$wbwindow.HTMLImageElement.prototype, 'src', 'im_');
    this.override_attr(this.$wbwindow.HTMLIFrameElement.prototype, 'src', 'if_');
    this.override_attr(this.$wbwindow.HTMLScriptElement.prototype, 'src', 'js_');
    this.override_attr(this.$wbwindow.HTMLVideoElement.prototype, 'src', 'oe_');
    this.override_attr(this.$wbwindow.HTMLVideoElement.prototype, 'poster', 'im_');
    this.override_attr(this.$wbwindow.HTMLAudioElement.prototype, 'src', 'oe_');
    this.override_attr(this.$wbwindow.HTMLAudioElement.prototype, 'poster', 'im_');
    this.override_attr(this.$wbwindow.HTMLSourceElement.prototype, 'src', 'oe_');
    this.override_attr(this.$wbwindow.HTMLSourceElement.prototype, 'srcset', 'oe_');
    this.override_attr(this.$wbwindow.HTMLInputElement.prototype, 'src', 'oe_');
    this.override_attr(this.$wbwindow.HTMLEmbedElement.prototype, 'src', 'oe_');
    this.override_attr(this.$wbwindow.HTMLObjectElement.prototype, 'data', 'oe_');

    this.override_anchor_elem();

    var style_proto = this.$wbwindow.CSSStyleDeclaration.prototype;

    // For FF
    if (this.$wbwindow.CSS2Properties) {
      style_proto = this.$wbwindow.CSS2Properties.prototype;
    }

    this.override_style_attr(style_proto, 'cssText');

    this.override_style_attr(style_proto, 'background', 'background');
    this.override_style_attr(style_proto, 'backgroundImage', 'background-image');

    this.override_style_attr(style_proto, 'listStyle', 'list-style');
    this.override_style_attr(style_proto, 'listStyleImage', 'list-style-image');

    this.override_style_attr(style_proto, 'border', 'border');
    this.override_style_attr(style_proto, 'borderImage', 'border-image');
    this.override_style_attr(style_proto, 'borderImageSource', 'border-image-source');
  }

  /**
   * @desc Override the HTMLAnchorElement properties
   */
  override_anchor_elem () {
    var anchor_orig = {};
    var Wombat = this;

    function save_prop (prop) {
      anchor_orig['get_' + prop] = get_orig_getter(Wombat.$wbwindow.HTMLAnchorElement.prototype, prop);
      anchor_orig['set_' + prop] = get_orig_setter(Wombat.$wbwindow.HTMLAnchorElement.prototype, prop);
    }

    for (var i = 0; i < this.URL_PROPS.length; i++) {
      save_prop(this.URL_PROPS[i]);
    }

    var anchor_setter = function (prop, value) {
      var func = anchor_orig['set_' + prop];
      if (func) {
        return func.call(this, value);
      } else {
        return '';
      }
    };

    var anchor_getter = function (prop) {
      var func = anchor_orig['get_' + prop];
      if (func) {
        return func.call(this);
      } else {
        return '';
      }
    };

    this.init_loc_override($wbwindow.HTMLAnchorElement.prototype, anchor_setter, anchor_getter);
    this.$wbwindow.HTMLAnchorElement.prototype.toString = function () {
      return this.href;
    };
  }

  /**
   * @param {HTMLElement|HTMLIFrameElement|HTMLStyleElement} elemtype
   * @param {string} prop the property name to override
   */
  override_html_assign (elemtype, prop) {
    if (!$wbwindow.DOMParser ||
      !elemtype ||
      !elemtype.prototype) {
      return;
    }

    var Wombat = this;

    var obj = elemtype.prototype;

    var orig_getter = get_orig_getter(obj, prop);
    var orig_setter = get_orig_setter(obj, prop);

    if (!orig_setter) {
      return;
    }

    var setter = function (orig) {
      var res = orig;
      if (!this._no_rewrite) {
        // init_iframe_insert_obs(this);
        if (this.tagName === 'STYLE') {
          res = Wombat.rewrite_style(orig);
        } else {
          res = Wombat.rewrite_html(orig);
        }
      }
      orig_setter.call(this, res);
    };

    def_prop(obj, prop, setter, orig_getter);
  }

  /**
   * @desc iframe.contentWindow and iframe.contentDocument overrides to ensure wombat is inited on the iframe $wbwindow
   * @param {string} prop
   */
  override_iframe_content_access (prop) {
    if (!this.$wbwindow.HTMLIFrameElement ||
      !this.$wbwindow.HTMLIFrameElement.prototype) {
      return;
    }

    var Wombat = this;

    var obj = this.$wbwindow.HTMLIFrameElement.prototype;

    var orig_getter = get_orig_getter(obj, prop);
    var orig_setter = get_orig_setter(obj, prop);

    if (!orig_getter) {
      return;
    }

    var getter = function () {
      Wombat.init_iframe_wombat(this);
      return orig_getter.call(this);
    };

    def_prop(obj, prop, orig_setter, getter);
    obj['_get_' + prop] = orig_getter;
  }

  /**
   * @desc Override $wbwindow.frames to ensure wombat is inited on them
   */
  override_frames_access () {
    this.$wbwindow.__wb_frames = this.$wbwindow.frames;
    var Wombat = this;
    var getter = function () {
      for (var i = 0; i < this.__wb_frames.length; i++) {
        Wombat.init_new_window_wombat(this.__wb_frames[i]);
      }
      return this.__wb_frames;
    };

    def_prop(this.$wbwindow, 'frames', undefined, getter);
    def_prop(this.$wbwindow.Window.prototype, 'frames', undefined, getter);
  }

  /**
   * @desc Init wombat on an iframe
   * @param {HTMLIFrameElement|HTMLFrameElement} iframe
   */
  init_iframe_wombat (iframe) {
    var win;

    if (iframe._get_contentWindow) {
      win = iframe._get_contentWindow.call(iframe);
    } else {
      win = iframe.contentWindow;
    }

    try {
      if (!win || win === this.$wbwindow || win._skip_wombat || win._wb_wombat) {
        return;
      }
    } catch (e) {
      return;
    }

    // var src = iframe.src;
    var src = this.wb_getAttribute.call(iframe, 'src');

    this.init_new_window_wombat(win, src);
  }

  init_new_window_wombat (win, src) {
    if (!win || win._wb_wombat) {
      return;
    }

    if (!src || src === '' || src === 'about:blank' || src.indexOf('javascript:') >= 0) {
      // win._WBWombat = wombat_internal(win);
      // win._wb_wombat = new win._WBWombat(wb_info);
      var wombat = new Wombat(win, this.wb_info)
      win._wb_wombat = wombat.wombat_init()
    } else {
      // These should get overriden when content is loaded, but just in case...
      // win._WB_wombat_location = win.location;
      // win.document.WB_wombat_location = win.document.location;
      // win._WB_wombat_top = $wbwindow.WB_wombat_top;

      this.init_proto_pm_origin(win);
      this.init_postmessage_override();
      this.init_messageevent_override();
    }
  }

  /**
   * @desc
   */
  init_doc_overrides () {
    if (!Object.defineProperty) {
      return;
    }

    if (this.$wbwindow.document._wb_override) {
      return;
    }

    var orig_referrer = this.extract_orig(this.$wbwindow.document.referrer);

    var domain_info;

    if (this.$wbwindow.wbinfo) {
      domain_info = this.$wbwindow.wbinfo;
    } else {
      domain_info = this.wbinfo;
    }

    domain_info.domain = domain_info.wombat_host;

    var domain_setter = function (val) {
      if (ends_with(domain_info.wombat_host, val)) {
        domain_info.domain = val;
      }
    };

    var domain_getter = function () {
      return domain_info.domain;
    };

    // changing domain disallowed, but set as no-op to avoid errors
    def_prop(this.$wbwindow.document, 'domain', domain_setter, domain_getter);

    def_prop(this.$wbwindow.document, 'referrer', undefined, function () { return orig_referrer; });

    // Cookies
    this.init_cookies_override();

    // Init mutation observer (for style only)
    // init_mutation_obs($wbwindow);

    // override href and src attrs
    this.attr_overrides();

    this.init_form_overrides();

    // Attr observers
    // if (!wb_opts.skip_attr_observers) {
    // init_href_src_obs($wbwindow);
    // }

    this.$wbwindow.document._wb_override = true;
  }

  /**
   * @desc Override the form action attribute of all forms in the page being replayed
   */
  init_form_overrides () {
    var Wombat = this;
    var do_init_forms = function () {
      for (var i = 0; i < Wombat.$wbwindow.document.forms.length; i++) {
        var new_action = Wombat.rewrite_url(Wombat.$wbwindow.document.forms[i].action);
        if (new_action !== Wombat.$wbwindow.document.forms[i].action) {
          Wombat.$wbwindow.document.forms[i].action = new_action;
        }
        Wombat.override_attr(Wombat.$wbwindow.document.forms[i], 'action', '', true);
      }
    };

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', do_init_forms);
    } else {
      do_init_forms();
    }
  }

  /**
   * @desc Initialize ``__WB_replay_top, __WB_top_frame, __WB_orig_parent, __WB_orig_parent``
   * to the correct values
   */
  init_top_frame () {
    // proxy mode
    if (this.wb_is_proxy) {
      this.$wbwindow.__WB_replay_top = this.$wbwindow.top;
      this.$wbwindow.__WB_top_frame = undefined;
      return;
    }

    var replay_top = this.$wbwindow;

    while ((replay_top.parent !== replay_top) && next_parent(replay_top.parent)) {
      replay_top = replay_top.parent;
    }

    this.$wbwindow.__WB_replay_top = replay_top;

    var real_parent = replay_top.__WB_orig_parent || replay_top.parent;

    // Check to ensure top frame is different window and directly accessible (later refactor to support postMessage)
    // try {
    //    if ((real_parent == $wbwindow) || !real_parent.wbinfo || !real_parent.wbinfo.is_frame) {
    //        real_parent = undefined;
    //    }
    // } catch (e) {
    //    real_parent = undefined;
    // }
    if (real_parent === this.$wbwindow || !this.wb_info.is_framed) {
      real_parent = undefined;
    }

    if (real_parent) {
      this.$wbwindow.__WB_top_frame = real_parent;

      this.init_frameElement_override(this.$wbwindow);
    } else {
      this.$wbwindow.__WB_top_frame = undefined;
    }

    // Fix .parent only if not embeddable, otherwise leave for accessing embedding window
    if (!this.wb_opts.embedded && (replay_top === this.$wbwindow)) {
      this.$wbwindow.__WB_orig_parent = this.$wbwindow.parent;
      this.$wbwindow.parent = replay_top;
    }
  }

  /**
   * @desc Init WombatLocation for the window
   * @param {Window} win
   */
  init_wombat_loc (win) {
    if (!win || (win.WB_wombat_location && win.document.WB_wombat_location)) {
      return;
    }

    // Location
    var wombat_location = new WombatLocation(win.location, this);

    if (Object.defineProperty) {
      var setter = function (value) {
        if (this._WB_wombat_location) {
          this._WB_wombat_location.href = value;
        } else {
          this.location = value;
        }
      };

      var getter = function () {
        if (this._WB_wombat_location) {
          return this._WB_wombat_location;
        } else {
          return this.location;
        }
      };

      def_prop(win.Object.prototype, 'WB_wombat_location', setter, getter);

      this.init_proto_pm_origin(win);

      win._WB_wombat_location = wombat_location;
      win.document._WB_wombat_location = wombat_location;
    } else {
      win.WB_wombat_location = wombat_location;
      win.document.WB_wombat_location = wombat_location;

      // Check quickly after page load
      setTimeout(this.check_all_locations, 500);

      // Check periodically every few seconds
      setInterval(this.check_all_locations, 500);
    }
  }

  /**
   * @desc Override Node.{appendChild,insertBefore,replaceChild}
   * - if the child is an instanceof window.Element {@link WombatRewriter#rewrite_elem}
   * - if the child is style {@link WombatRewriter#rewrite_style}
   * - if the child is an iframe {@link WBWombat#init_iframe_wombat}
   */
  override_dom_functions () {
    if (!this.$wbwindow.Node || !this.$wbwindow.Node.prototype) {
      return;
    }

    var Wombat = this;

    function replace_dom_func (funcname) {
      var orig = Wombat.$wbwindow.Node.prototype[funcname];
      Wombat.$wbwindow.Node.prototype[funcname] = function () {
        var child = arguments[0];
        if (child) {
          if (child instanceof Wombat.$wbwindow.Element) {
            Wombat.rewrite_elem(child);
          } else if (child instanceof Wombat.$wbwindow.Text) {
            if (this.tagName === 'STYLE') {
              child.textContent = Wombat.rewrite_style(child.textContent);
            }
          }
        }
        var created = orig.apply(this, arguments);
        if (created && created.tagName === 'IFRAME') {
          Wombat.init_iframe_wombat(created);
        }
        return created;
      };
    }

    replace_dom_func('appendChild');
    replace_dom_func('insertBefore');
    replace_dom_func('replaceChild');
  }

  /**
   * @desc setup wombat
   * @return {{extract_orig: function,rewrite_url: function,watch_elem: function,
    init_new_window_wombat: function,init_paths: function}}
   */
  wombat_init () {

    this.init_top_frame();

    this.init_wombat_loc(this.$wbwindow);

    // archival mode: init url-rewriting intercepts
    if (!this.wb_is_proxy) {
      this.init_wombat_top();

      if (this.wb_replay_prefix && this.wb_replay_prefix.indexOf(this.$wbwindow.__WB_replay_top.location.origin) === 0) {
        this.wb_rel_prefix = this.wb_replay_prefix.substring(this.$wbwindow.__WB_replay_top.location.origin.length + 1);
      } else {
        this.wb_rel_prefix = this.wb_replay_prefix;
      }
      this.wb_rel_prefix_check = this.wb_rel_prefix;

      // if ($wbwindow.opener) {
      //    $wbwindow.opener.WB_wombat_location = copy_location_obj($wbwindow.opener.location);
      // }

      // Domain
      // $wbwindow.document.WB_wombat_domain = wbinfo.wombat_host;
      // $wbwindow.document.WB_wombat_referrer = extract_orig($wbwindow.document.referrer);

      this.init_doc_overrides();

      // History
      this.override_history_func('pushState');
      this.override_history_func('replaceState');

      this.override_history_nav('go');
      this.override_history_nav('back');
      this.override_history_nav('forward');

      // postMessage
      // OPT skip
      if (!this.wb_opts.skip_postmessage) {
        this.init_postmessage_override();
        this.init_messageevent_override();
      }

      this.init_hash_change();

      // write
      this.override_document_write();

      // eval
      // this.init_eval_override();

      // Ajax
      this.override_ajax();

      // Fetch
      this.init_fetch_rewrite();

      // Worker override (experimental)
      this.init_web_worker_override();
      this.init_service_worker_override();

      // innerHTML can be overriden on prototype!
      this.override_html_assign(this.$wbwindow.HTMLElement, 'innerHTML');
      this.override_html_assign(this.$wbwindow.HTMLIFrameElement, 'srcdoc');
      this.override_html_assign(this.$wbwindow.HTMLStyleElement, 'textContent');

      // Document.URL override
      this.override_prop_extract(this.$wbwindow.Document.prototype, 'URL');
      this.override_prop_extract(this.$wbwindow.Document.prototype, 'documentURI');

      // Attr nodeValue and value
      this.override_attr_props();

      // init insertAdjacentHTML() override
      this.init_insertAdjacentHTML_override();

      // iframe.contentWindow and iframe.contentDocument overrides to
      // ensure wombat is inited on the iframe $wbwindow!
      this.override_iframe_content_access('contentWindow');
      this.override_iframe_content_access('contentDocument');

      this.override_frames_access();

      // base override
      this.override_htmlBaseElem_nodeBaseURI();

      // setAttribute
      if (!this.wb_opts.skip_setAttribute) {
        this.init_setAttribute_override();
        this.init_getAttribute_override();
      }

      // createElement attr override
      if (!this.wb_opts.skip_createElement) {
        this.override_createElement();
      }

      // ensure namespace urls are NOT rewritten
      this.init_createElementNS_fix();

      // Image
      // this.init_image_override();

      // DOM
      // OPT skip
      if (!this.wb_opts.skip_dom) {
        this.override_dom_functions();
      }

      // registerProtocolHandler override
      this.init_registerPH_override();

      // sendBeacon override
      this.init_beacon_override();
    }

    // other overrides
    // proxy mode: only using these overrides

    // Random
    this.init_seeded_random(this.wbinfo.wombat_sec);

    // Crypto Random
    this.init_crypto_random();

    // Date
    this.init_date_override(this.wbinfo.wombat_sec);

    // open
    this.override_window_open();

    // disable notifications
    this.init_disable_notifications();

    // expose functions
    var obj = {};
    obj.extract_orig = this.extract_orig.bind(this);
    obj.rewrite_url = this.rewrite_url.bind(this);
    obj.watch_elem = this.watch_elem.bind(this);
    obj.init_new_window_wombat = this.init_new_window_wombat.bind(this);
    obj.init_paths = this.init_paths.bind(this);

    return obj;
  }

  /**
   * @desc Utility functions used by rewriting rules
   * @param elem
   * @param func
   * @return {boolean}
   */
  watch_elem (elem, func) {
    if (!$wbwindow.MutationObserver) {
      return false;
    }

    var m = new MutationObserver(function (records, observer) {
      for (var i = 0; i < records.length; i++) {
        var r = records[i];
        if (r.type === 'childList') {
          for (var j = 0; j < r.addedNodes.length; j++) {
            func(r.addedNodes[j]);
          }
        }
      }
    });

    m.observe(elem, {
      childList: true,
      subtree: true
    });
  }

  /**
   * @desc Update the WombatLocation
   * @param {string} req_href
   * @param {string} orig_href
   * @param {Location} actual_location
   * @param {WombatLocation} wombat_loc
   */
  update_location (req_href, orig_href, actual_location, wombat_loc) {
    if (!req_href) {
      return;
    }

    if (req_href === orig_href) {
      // Reset wombat loc to the unrewritten version
      //if (wombat_loc) {
      //    wombat_loc.href = extract_orig(orig_href);
      //}
      return;
    }

    var ext_orig = this.extract_orig(orig_href);
    var ext_req = this.extract_orig(req_href);

    if (!ext_orig || ext_orig === ext_req) {
      return;
    }

    var final_href = this.rewrite_url(req_href);

    console.log(actual_location.href + ' -> ' + final_href);

    actual_location.href = final_href;
  }

  /**
   * @desc Check if the location has changed
   * @param {WombatLocation} wombat_loc
   * @param {boolean} is_top
   */
  check_location_change (wombat_loc, is_top) {
    var locType = (typeof wombat_loc);

    var actual_location = (is_top ? this.$wbwindow.__WB_replay_top.location : this.$wbwindow.location);

    // String has been assigned to location, so assign it
    if (locType === "string") {
      this.update_location(wombat_loc, actual_location.href, actual_location);

    } else if (locType === "object") {
      this.update_location(wombat_loc.href,
        wombat_loc._orig_href,
        actual_location);
    }
  }

  /**
   * @desc check replay-iframe location and replay_top location
   * @return {boolean}
   */
  check_all_locations () {
    if (this.wb_wombat_updating) {
      return false;
    }

    this.wb_wombat_updating = true;

    this.check_location_change(this.$wbwindow.WB_wombat_location, false);

    // Only check top if its a different $wbwindow
    if (this.$wbwindow.WB_wombat_location !== this.$wbwindow.__WB_replay_top.WB_wombat_location) {
      this.check_location_change(this.$wbwindow.__WB_replay_top.WB_wombat_location, true);
    }

    //        lochash = $wbwindow.WB_wombat_location.hash;
    //
    //        if (lochash) {
    //            $wbwindow.location.hash = lochash;
    //
    //            //if ($wbwindow.top.update_wb_url) {
    //            //    $wbwindow.top.location.hash = lochash;
    //            //}
    //        }

    this.wb_wombat_updating = false;
  }
}

module.exports = Wombat