16

In my Chrome extension, I want to have my options.html page communicate with something like Google's OpenId API. In order to do this seamlessly, I have a hidden iframe on the options page which will pop-up the Google Accounts login page (following the OpenId interaction sequence, etc.).

My issue is that I can't communicate from the options page to the iframe (the origin of the iframe is something I control, but not the same as my chrome extension) via window.postMessage. I was wondering if there is a quick workaround to this issue.

If there isn't, I'm going to make options.html contain an iframe that houses the layout and logic of the page.

Rob W
  • 341,306
  • 83
  • 791
  • 678
yuzeh
  • 442
  • 1
  • 5
  • 12

2 Answers2

42

You don't have to mess with iframes. It's possible to perform cross-domain XMLHttpRequests, using background pages. Since Chrome 13, cross-site requests can be made from the content script. However, requests can still fail if the page is served with a Content Security Policy header with a restricting connect-src.

Another reason for choosing the nexy method over content scripts is that requests to http sites will cause a mixed content warning ("The page at https://... displayed insecure content from http://...").

Yet another reason for delegating the request to the background page is when you want to get a resource from the file://, because a content script cannot read from file:, unless it is running on a page at the file:// scheme.

Note
To enable cross-origin requests, you have to explicitly grant permissions to your extension using the permissions array in your manifest file.

Cross-site request using background script.

The content script would request the functionality from the background via the messaging API. Here is an example of a very simple way of sending and getting the response of a request.

chrome.runtime.sendMessage({
    method: 'POST',
    action: 'xhttp',
    url: 'http://www.stackoverflow.com/search',
    data: 'q=something'
}, function(responseText) {
    alert(responseText);
    /*Callback function to deal with the response*/
});

Background / event page:

/**
 * Possible parameters for request:
 *  action: "xhttp" for a cross-origin HTTP request
 *  method: Default "GET"
 *  url   : required, but not validated
 *  data  : data to send in a POST request
 *
 * The callback function is called upon completion of the request */
chrome.runtime.onMessage.addListener(function(request, sender, callback) {
    if (request.action == "xhttp") {
        var xhttp = new XMLHttpRequest();
        var method = request.method ? request.method.toUpperCase() : 'GET';

        xhttp.onload = function() {
            callback(xhttp.responseText);
        };
        xhttp.onerror = function() {
            // Do whatever you want on error. Don't forget to invoke the
            // callback to clean up the communication port.
            callback();
        };
        xhttp.open(method, request.url, true);
        if (method == 'POST') {
            xhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        }
        xhttp.send(request.data);
        return true; // prevents the callback from being called too early on return
    }
});

Remark: The messaging APIs have been renamed several times. If your target browser is not the latest Chrome version, check out this answer.

For completeness, here's a manifest file to try out my demo:

{
    "name": "X-domain test",
    "manifest_version": 2,
    "permissions": [
        "http://www.stackoverflow.com/search*"
    ],
    "content_scripts": {
        "js": ["contentscript.js"],
        "matches": ["http://www.example.com/*"]
    },
    "background": {
        "scripts": ["background.js"],
        "persistent": false
    }
}
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • Thanks for this - it was helpful, but you may want to update it as your background event page has a couple problems. xhttp.open() must be place before the POST setRequestHeader or it will error. Also, there is no reason to set Content-length as it will just be refused. – ElJeffe Aug 11 '14 at 17:32
  • Any suggestions on adding a timeout / retry to this that would work with the async nature, which might have multiple queries running at once? – ElJeffe Sep 19 '14 at 16:54
  • @JeffG Could you be a bit more specific? – Rob W Sep 19 '14 at 16:59
  • Sometimes the XMLHttpRequest can hang as there is no timeout set. So I was looking for a way to set a timeout, then retry up to 3 times. I just found this: http://stackoverflow.com/questions/1523686/timeout-xmlhttprequest which might help, but not sure about determining the number of tries within each request as it can't be an external variable due to the async nature. – ElJeffe Sep 19 '14 at 19:46
  • @JeffG If you want to have some back-and-forths between the content script and background page, use [`chrome.runtime.connect`](https://developer.chrome.com/extensions/runtime#method-connect) and [`chrome.runtime.onConnect`](https://developer.chrome.com/extensions/runtime#event-onConnect) instead of sendMessage/onMessage. – Rob W Sep 19 '14 at 19:48
  • When I try the first method, it shows this: extensions::runtime:143 Uncaught Error: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument -- any ideas? – Roman Gaufman Dec 04 '16 at 12:27
  • @RomanGaufman The code must be called from a [content script](https://developer.chrome.com/extensions/content_scripts), not from a web page. – Rob W Dec 04 '16 at 12:38
  • @RobW Sometimes I get this error in the background console: Uncaught Error: Attempting to use a disconnected port object at PortImpl.postMessage (extensions::messaging:60:13) at Port.publicClassPrototype.(anonymous function) [as postMessage] (extensions::utils:149:26) at responseCallback (extensions::messaging:166:16) at XMLHttpRequest.xhttp.onload (chrome-extension://.../background.js:20:13) – ElJeffe Mar 04 '17 at 14:29
  • @JeffG That may happen if you unload the page/frame before the HTTP response was fully received. – Rob W Mar 04 '17 at 14:31
  • @RobW If I understand, the act of watching the background console creates the error as I've changed focus from the extension popup for which the message is sent. Is that correct? – ElJeffe Mar 04 '17 at 15:07
  • 1
    @JeffG If you call `chrome.runtime.sendMessage` from the popup and then inspect the background page (causing the popup to close) before the HTTP response was received, then `sendResponse` will indeed fail as the popup context has closed. – Rob W Mar 04 '17 at 15:21
  • This example would not work for me when matches was set to only execute on example.com. It pops up with NULL instead of the expected search results html. I changed matches in my manifest to all web pages as such and it works perfectly !! BUT I HAVE NO IDEA WHY. IT SHOULD NOT BE THIS WAY. Here is my mod for anyone trying to get this to actually work: "matches": [ "http://*/*", "https://*/*" ] – user1254723 Apr 16 '19 at 12:05
3

I implemented the same thing using jquery its much simpler and it worked great too..

background.js

chrome.runtime.onMessage.addListener(function(request, sender, callback) {
  if (request.action == "xhttp") {

    $.ajax({
        type: request.method,
        url: request.url,
        data: request.data,
        success: function(responseText){
            callback(responseText);
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            //if required, do some error handling
            callback();
        }
    });

    return true; // prevents the callback from being called too early on return
  }
});

contentscript.js

chrome.runtime.sendMessage({
        method: 'POST',
        action: 'xhttp',
        url: 'http://example-url.com/page.php',
        data: "key=value"
    }, function(reponseText) {
        alert(responseText);
    }); 

But make sure manifest.json file has required permissions and jquery js file

  "permissions": [
      "tabs", "activeTab", "http://example-url.com/*"
  ],
  "content_scripts": [ {
      "js": [ "jquery-3.1.0.min.js", "contentscript.js" ],
      "matches": [ "https://example-ssl-site.com/*" ]
  }],
  "background": {
      "scripts": [ "jquery-3.1.0.min.js", "background.js" ]
  }
kishoreballa
  • 67
  • 1
  • 3