4

Refer to the technique of having the same JavaScript to run in both a web page and an iframe, as described in this answer:

For example, suppose you have this page at domain_A.com:

<html>
<body>
    <iframe src="http://domain_B.com/SomePage.htm"></iframe>
</body>
</html>

If you set your @match directives like this:

// @match http://domain_A.com/*
// @match http://domain_B.com/*

Then your script will run twice -- once on the main page and once on the iframe as though it were a standalone page.


What are the options to have the two instances of the script to communicate with each other?

This would be needed to sync the instances. For example, have the iframe script-instance execute its task only after the webpage script-instance completed, and vice versa.

Community
  • 1
  • 1
Exilas
  • 49
  • 1
  • 2

1 Answers1

7

The two script instances can communicate with each other using postMessage(). Regarding:

This would be needed to sync the instances, so for example to have the iframe one to execute its task only after the webpage one completed, and vice versa.

That's what is shown in this other answer.
But Chrome has bugs in how it presents frames/iframes to extensions So, to work around these bugs, you must inject the code that calls postMessage().

The following script shows how. It:

  • Runs in both an iframe and the containing page.
  • Handles cross domain iframes.
  • It demonstrates inter-script control having the following logic:
    1. The container page sets up to listen for messages from the iframe.
    2. The iframe sets up to listen for messages from the container page.
    3. The iframe sends the first message to the container page.
    4. When the container page receives that message, it sends another message back to the iframe.

Install this script (Updated thanks to CertainPerformance, for changes in the target sites over the years):

// ==UserScript==
// @name        _Cross iframe, cross-domain, interscript communication
// @include     http://fiddle.jshell.net/2nmfk5qs/*
// @include     http://puppylinux.com/
// @grant       none
// ==/UserScript==
/* eslint-disable no-multi-spaces */

if (window.top === window.self) return;
console.log ("Script start...");
if (window.location.href.includes('fiddle')) {
    console.log ("Userscript is in the MAIN page.");

    //--- Setup to process messages from the GM instance running on the iFrame:
    window.addEventListener ("message", receiveMessageFromFrame, false);
    console.log ("Waiting for Message 1, from iframe...");
}
else {
    console.log ("Userscript is in the FRAMED page.");

    //--- Double-check that this iframe is on the expected domain:
    if (/puppylinux\.com/i.test (location.host) ) {
        window.addEventListener ("message", receiveMessageFromContainer, false);

        //--- Send the first message to the containing page.
        sendMessageFromAnIframe (
            "***Message 1, from iframe***", "http://fiddle.jshell.net"
        );
        console.log ("Waiting for Message 2, from containing page...");
    }
}

function receiveMessageFromFrame (event) {
    if (event.origin != "http://puppylinux.com")    return;

    console.log ('The container page received the message, "' + event.data + '".');

    //--- Send message 2, back to the iframe.
    sendMessageToAnIframe (
        "#testIframe",
        "***Message 2, from the container page***",
        "http://puppylinux.com"
    );
}

function receiveMessageFromContainer (event) {
    if (event.origin != "http://fiddle.jshell.net")    return;

    console.log ('The iframe received the message, "' + event.data + '".');
}

/*--- Because of bugs in how Chrome presents frames to extensions, we must inject
    the messaging code. See bug 20773 and others.
    frames, top, self.parent, contentWindow, etc. are all improperly undefined
    when we need them.  See Firefox and other browsers for the correct behavior.
*/
function sendMessageFromAnIframe (message, targetDomain) {
    var scriptNode          = document.createElement ('script');
    scriptNode.textContent  = 'parent.postMessage ("' + message
                            + '", "' + targetDomain + '");'
                            ;
    document.body.appendChild (scriptNode);
}

function sendMessageToAnIframe (cssSelector, message, targetDomain) {
    function findIframeAndMessageIt (cssSelector, message, targetDomain) {
        var targetIframe    = document.querySelector (cssSelector)
        if (targetIframe) {
            targetIframe.contentWindow.postMessage (message, targetDomain);
        }
    }
    var scriptNode          = document.createElement ('script');
    scriptNode.textContent  = findIframeAndMessageIt.toString ()
                            + 'findIframeAndMessageIt ("' + cssSelector
                            + '", "' + message
                            + '", "' + targetDomain + '");'
                            ;
    document.body.appendChild (scriptNode);
}

console.log ("Script end");


Then visit this test page at jsFiddle.

You will see this in the javascript console:

Script start...
Userscript is in the MAIN page.
Waiting for Message 1, from iframe...
Script end
Script start...
Userscript is in the FRAMED page.
Waiting for Message 2, from containing page...
Script end
The container page received the message, "***Message 1, from iframe***".
The iframe received the message, "***Message 2, from the container page***".
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • Brock, I just come back from vacation and saw your answer. Thank you very much, I will study it in detail and try to apply it to my case. – Exilas Aug 20 '12 at 23:31
  • 1) As of now, is postMessage usable in chrome? You mentioned that the code would need to be injected because chrome extensions have problems. Is this still the case? 2) As an extension to the original question about postMessage use in userscripts, is it possible for the website itself to intercept postMessage use in a userscript? – Edge Mar 13 '15 at 09:37
  • @Edge: Yes and yes (but if you used the script in Tampermonkey and set `@grant none`, injection *might* not be needed). And yes, in many cases, the web page can intercept the postMessages. I've yet to see a *real* site that was that anal/paranoid though. – Brock Adams Mar 13 '15 at 10:56
  • The example doesn't work anymore - it looks like JSFiddle changed their presentation so that the code proper is in an iframe no matter what (so even going to `.../show/light` results in a page with... an embedded iframe linking to `.../show/light`), so the `window.top` test doesn't behave as expected. puppylinux also does not use www anymore. – CertainPerformance Apr 09 '19 at 11:00
  • https://gist.github.com/CertainPerformance/91c962a1205a3d25f56dfd340f1a8f7f running on http://fiddle.jshell.net/2nmfk5qs/show/light/ looks to work on TM – CertainPerformance Apr 09 '19 at 11:11
  • 1
    Thanks, @CertainPerformance. Updated the answer with your fixes. Nowadays, I'd probably use `GM_addValueChangeListener` instead (now that it exists) as `postMessage` has been somewhat fiddly. – Brock Adams Apr 09 '19 at 22:06
  • @Brock Adams: ViolentMonkey on FF96 has some limitations so nothing is getting sent or received regardless of whether I use injection or directly or whether I put an exact literal URL as `targetOrigin` or just '*'. So `GM_addValueChangeListener` seems to be the only way. – DDRRSS Jan 03 '22 at 09:20