Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Chrome extensions have the ability to intercept all web requests to specified URLs using chrome.webRequest.onBeforeRequest. This includes not only static asset requests, but requests for AJAX, PJAX, favicons, and everything in between.

Apple provides a few close approximations to this functionality, such as the beforeLoad (handles images, CSS, and JS) and beforeNavigate (handles full page loads) event handlers, but neither catch AJAX requests. I've tried overloading XMLHttpRequest in an attempt to catch AJAX loads to no avail (I might be doing something wrong). Here's a brief example of how I'm doing this:

var originalOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
    console.log("overriden");
    return originalOpen.apply(this, arguments);
}

How can I catch all web requests (AJAX, CSS, JS, etc.) in a Safari extension?

like image 234
dwlz Avatar asked Jan 20 '15 23:01

dwlz


People also ask

Does Safari support Manifest JSON?

Safari 15.4 and later supports manifest versions 2 and 3. To evaluate compatibility for manifest keys in your extension, see Mozilla's compatibility table at Browser compatibility for manifest. json.

What is the API used for webRequestBlocking?

To use the webRequest API for a given host, an extension must have the "webRequest" API permission and the host permission for that host. To use the "blocking" feature, the extension must also have the "webRequestBlocking" API permission.


1 Answers

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;
like image 79
qdev Avatar answered Oct 13 '22 11:10

qdev