Writing an equivalent to Chrome's onBeforeRequest in a Safari extension Writing an equivalent to Chrome's onBeforeRequest in a Safari extension google-chrome google-chrome

Writing an equivalent to Chrome's onBeforeRequest in a Safari extension


Update: You can check entire code flow on my first Safari Extension I've wrote for TimeCamp tracker: https://github.com/qdevro/timecamp.safariextz

I have succeeded to intercept all AJAX calls (actually the responses were interesting for me, because there all the magic happens), but unfortunately I couldn't find (yet) a solution to send it back to my injected script (I still work on this) now fully working - getting the xhr to the injected script:

I've done it like this:

1) on the injected START script, I've added into the DOM another script (the one which does the interception):

    $(document).ready(function(){      var script = document.createElement('script');      script.type = 'text/javascript';      script.src = safari.extension.baseURI + 'path/to/your/script/bellow.js';      document.getElementsByTagName('head')[0].appendChild(script);    })

2) the interception code uses this repository as override of the XMLHttpRequest object, that I've tweaked a little bit as well in order to attach the method, url and sent data to it in order to be easily available when the response get's back.

Basically, I've overriden the open() method of the XMLHttpsRequest to attach those values that I might need in my script, and added the sentData in the send() method as well:

    var RealXHROpen = XMLHttpRequest.prototype.open;    ...    // Override open method of all XHR requests (inside wire() method    XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {      this.method = method;      this.url = url;       RealXHROpen.apply(this, arguments);    }    ...    // Override send method of all XHR requests    XMLHttpRequest.prototype.send = function(sentData) {       ...       this.sentData = sentData;       ...    }

Then, I've added a callback on the response, which get's a modified XMLHttpRequest object WHEN the data comes back, and cotains everything: url, method, sentData and responseText with the retrieved data:

    AjaxInterceptor.addResponseCallback(function(xhr) {      console.debug("response",xhr);      // xhr.method - contains the method used by the call      // xhr.url - contains the URL was sent to      // xhr.sentData - contains all the sent data (if any)      // xhr.responseText - contains the data sent back to the request      // Send XHR object back to injected script using DOM events      var event = new CustomEvent("ajaxResponse", {        detail: xhr      });      window.dispatchEvent(event);    });    AjaxInterceptor.wire();

For sending the XHR object from the intercept script back to the injected script, I just had to use DOM events like @Xan has suggested (thanks for that):

    window.addEventListener("ajaxResponse", function(evt) {      console.debug('xhr response', evt.detail);      // do whatever you want with the XHR details ...    }, false);

Some extra hints / (workflow) optimisations that I've used in my project:

  • I've cleaned the GET url's and moved all the parameters (? &) into the dataSent property;
  • I've merged this dataSent property if there's the case (in send(data) method)
  • I've added an identifier on request send (timestamp) in order to match it later (see point bellow and get the idea);
  • I've sent a custom event to the script called "ajaxRequest" in order to prepare / optimise load times (I had to request some other data to some external API using CORS - by passing the call / response back and forth to the global.html which is capable of handling CORS), so I didn't had to wait for the original request to come back before sending my API call, but just matching the responses based on timestamp above;