Intercept XHR and make it downloadable with page/browser action popup menu

As in title, I want to intercept data sent/received with XHR and then download it to file. I know that this intercepting code must be injected to the page. But how do I communicate between injected code and page/browser action popup? I want to use as little permissions as I can. What’s the best way to do it?

activeTab seems like permission I want to use but how to make it work when I click on menu entry in page/browser action popup?

You can also use https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/filterResponseData to intercept the body of a request.

You can communicate between code using messaging, if you are replacing XHR, you should use https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Sharing_objects_with_page_scripts instead, which means that you can just use the runtime messaging from the content script to the rest of the extension.

I think webRequest API would solve the problem with XHR but I also need to intercept any URL changes so Sharing objects with page scripts is better approach anyway. But how exactly do I use this?

((realPushState) => {
  window.wrappedJSObject.history.pushState = function () {
    console.debug(arguments[2]);
    realPushState.apply(window.wrappedJSObject.history, arguments);
  };
})(window.wrappedJSObject.history.pushState);

in content script throws Permission denied to access object error.

As the page I linked mentions, you need to export/clone things into the page context. Further, URL changes could be observed with webNavigation.

Alright, I think I’ve got it now.
content-script.js:

// listen to messages from popup
browser.runtime.onMessage.addListener((message, sender) => {
  // ...
})

const extensionObject = {
  historyPushState: function(url) {
    // ...
  },
  xhrOpen: function(url, xhr) {
    // you can add `load` event listener to xhr to get response
    // return `true` if you want to capture data in function below
  },
  xhrSend: function(url, data) {
    // first element of result array tells if you want to send this xhr
    // second is data you want to send
    let result = [true, data];
    return cloneInto(result, window);
  }
};

window.wrappedJSObject.extensionObject = cloneInto(extensionObject, window, {cloneFunctions: true});

{
  // inject script to page scope
  const s = document.createElement('script');
  s.src = browser.extension.getURL('inject.js');
  s.onload = () => {
    document.head.removeChild(s);
  };
  document.head.appendChild(s);
}

inject.js:

((extensionObject, realOpen, realSend, realPushState) => {
  delete window.extensionObject;
  const requestsToIntercept = {};

  XMLHttpRequest.prototype.open = function(method, url) {
    const interceptSend = extensionObject.xhrOpen(url, this);
    if(interceptSend)
      requestsToIntercept[this] = url;
    realOpen.apply(this, arguments);
  };

  XMLHttpRequest.prototype.send = function(data) {
    const url = requestsToIntercept[this];
    let sendRequest = true;
    let newData = data;
    if(typeof url === 'string') {
      delete requestsToIntercept[this];
      [sendRequest, newData] = extensionObject.xhrSend(url, newData);
    }
    if(sendRequest)
      realSend.apply(this, [newData]);
  };

  history.pushState = function(obj, title, url) {
    extensionObject.historyPushState(url);
    realPushState.apply(history, arguments);
  };
})(window.extensionObject, XMLHttpRequest.prototype.open,
  XMLHttpRequest.prototype.send, history.pushState);

And since I store all data in content-script.js I just need to communicate between browser/page action popup and content script:
popup.js:

// get active tab
browser.tabs.query({active:true,currentWindow:true}).then(tabs => {
  browser.tabs.sendMessage(tabs[0].id, {message: "hello"});
});

This doesn’t even require activeTab permission. :smiley:

Is this okay or should I do it another way?