21

I'm working on a website with cross-domain iframes that are resized to the correct height using postMessage. The only problem I'm having is identifying which iframe has which height. The way I've currently got it set up is that when one iframe sends its height to the parent, all the iframes' heights are changed.

Parent:

var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";

eventer(messageEvent, function(e) {
    $('iframe').height(e.data);
}, false);

Iframe:

var updateHeight = function() {
    if(window.parent) {
        window.parent.postMessage($('.widget').outerHeight(), '*');
    }
};

Is there some way to identify which iframe sent the message event?

skimberk1
  • 1,984
  • 3
  • 19
  • 26

6 Answers6

23

Yes, you can identify the IFRAME which did the postMessage. There are different situations:

  • the source IFRAME has the same-origin URL (e.g. http://example.com/) as the Window which receives the message: the IFRAME is identified using

    myIFRAME.contentWindow == event.source

  • the source IFRAME has a same-origin but relative URL (e.g. /myApp/myPage.html) to the parent HTML page: the IFRAME is identified using

    myIFRAME.contentWindow == event.source.parent

  • the source IFRAME has a cross-origin URL (e.g. http://example.com/) different of the page which receives the message (e.g http://example.org/): the above methods do not work (the comparison is always false and accessing properties of event.source lead to Access Deniederrors) and the IFRAME must be identified based on its origin domain;

    myIFRAME.src.indexOf(event.origin)==0

In order to manage these three different situations, I'm using the following:

var sourceFrame = null; // this is the IFRAME which send the postMessage
var myFrames = document.getElementsByTagName("IFRAME");
var eventSource = event.source; // event is the event raised by the postMessage
var eventOrigin = event.origin; // origin domain, e.g. http://example.com

// detect the source for IFRAMEs with same-origin URL
for (var i=0; i<myFrames.length; i++) {
    var f = myFrames[i];
    if (f.contentWindow==eventSource || // for absolute URLs
        f.contentWindow==eventSource.parent) { // for relative URLs
        sourceFrame = f;
        break;
    }
}

// detect the source for IFRAMEs with cross-origin URL (because accessing/comparing event.source properties is not allowed for cross-origin URL)
if (sourceFrame==null) {
    for (var i=0; i<myFrames.length; i++) {
        if (myFrames[i].src.indexOf(eventOrigin)==0) {
            sourceFrame = myFrames[i];
            break;
        }
    }
}

For cross-domain URLs, note that we cannot differentiate the true source if event.origin is a domain common to more than one IFRAMEs.

Some people use === instead of == but I did not found any difference in this context, so I'm using the shortest comparator.

This implementation has been tested and works under:

  • MSIE 9
  • Firefox 17

As an alternative (suggested by Griffin), you could use a IFRAME src with an unique idenfitier (e.g. timestamp), and the IFRAME'd web application will send back the this unique identifier in the posted messages. While the IFRAME identification would be simpler, this approach requires to modify the IFRAME'd web application (which is not always possible). This also may lead to security issues (e.g. the IFRAME'd web application tries to guess the unique identifier of other IFRAMEs applications).

Julien Kronegg
  • 4,282
  • 42
  • 53
  • 2
    you would probably be better off just making sure each iframe has a unique href using a timestamp query string, and then including the source href in the cross-document messages. – Griffin Jul 29 '14 at 23:56
  • @Griffin: you are right, but there are some drawbacks. I edited the answer to add your alternative and upvoted your comment. – Julien Kronegg Aug 26 '14 at 14:06
  • what's not allowed is accessing eventSource.parent. comparing to eventSource should be fine. – Collin Anderson Feb 10 '16 at 23:27
  • 1
    @JulienKronegg Are you sure `myIFRAME.contentWindow == event.source` didn't work for the second case? `ev.source.parent` looks like a message from two levels of embedding. – leewz Sep 15 '16 at 20:09
  • I wrote this code about 3 years ago for a complex application framework at one of my previous customer (so I don't have the code anymore). I had issues with relative URLs, so I had to find the `ev.source.parent` workaround. As far as I remember, the `postMessage` behaves differently for relative URLs and gives a deeper HTMLElement as source, thus the `.parent`. – Julien Kronegg Sep 15 '16 at 21:42
  • @JulienKronegg I got an issue in iOS 10 where iframes with (I think) relative URLs will set `ev.source` to the parent window instead of the frame window. Though that's the opposite of what you got, and it didn't happen in iOS 9. I am working around it with `ev.source == window`. Incredibly, the `ev` object there has absolutely NO information that allows true source identification. – leewz Oct 13 '16 at 20:26
  • Submitted a bug report for the iOS 10 issue: https://bugs.webkit.org/show_bug.cgi?id=163412 – leewz Oct 13 '16 at 22:45
  • I would add to what Griffin said, that unique identifiers (e.g. timestamps) are not an alternative but is a must if you have many `iframe`s with the same origin. So, it's rather not a suggestion. It doesn't have to be a query string attached id but it can also be key exchange between windows stored in `iframe` element attribute for selector sake. – Krzysztof Przygoda Mar 20 '18 at 14:21
  • Moreover, it seems that origin does not contain any query string... even a path. – Krzysztof Przygoda Mar 20 '18 at 15:43
  • Based on this the answer should be "No, it is not possible in the general case" – Tamas Hegedus Nov 08 '19 at 15:21
20

I have found the solution from here: How to share a data between a window and a frame in JavaScript

Parent:

var frames = document.getElementsByTagName('iframe');
for (var i = 0; i < frames.length; i++) {
    if (frames[i].contentWindow === event.source) {
        $(frames[i]).height(event.data); //the height sent from iframe
        break;
    }
}
Community
  • 1
  • 1
Jack
  • 319
  • 2
  • 7
  • The comparison to `event.source` does not work in all situations. I had to put a fallback condition such as `if (frames[i].src.indexOf(event.origin)==0) {` in order to make the IFRAME identification more robust. – Julien Kronegg Oct 28 '13 at 14:26
  • 1
    @JulienKronegg That's actually less robust, as it will match any iframe with a src on the same origin (unless you're doing this in addition to). Which situations? I'm having an issue matching in FF. – Randy Hall Nov 27 '13 at 19:26
  • 2
    @RandyHall: Yes, its in addition, in order to manage cross-domain issues. I promoted my comment to an answer for better understanding, see http://stackoverflow.com/a/20404180/698168 – Julien Kronegg Dec 05 '13 at 15:41
  • @JulienKronegg where did you experince that problem? i cannot reproduce it in IE10, Chrome33 or iOS7 – Jörn Berkefeld Apr 04 '14 at 15:51
  • @JörnBerkefeld: the issue occurs for cross-origin URL, see [my answer](http://stackoverflow.com/a/20404180/698168) – Julien Kronegg Apr 07 '14 at 13:21
  • yes i tried that but could not confirm what you stated - hence my question on where (which browsers/os) you noticed it ;-) – Jörn Berkefeld Apr 07 '14 at 14:31
  • Might want to rename the `frames` variable to something else. Otherwise it will overwrite the global `frames` variable (a.k.a `window.frames`) or shadow it, depending on whether the code is top-level or not. – Miscreant Apr 27 '17 at 15:22
  • @Miscreant Shouldn't you access that variable via `window.` anyway? – Florian Wendelborn Jun 29 '17 at 22:23
  • @Dodekeract `frames` is a writable property of `window`, so if this answer's code is top-level (not inside a function), it will overwrite the `window.frames` property. – Miscreant Jun 30 '17 at 03:00
  • ... or at least on Chrome `window.frames` is writable. It's also noteworthy that `window.frames` is just an alias for `window` itself (i.e. `window.frames === window`), so you can access the first frame through `window[0]` instead of `window.frames[0]`. But overwriting `window.frames` seems like a bad idea that can lead to bugs and head scratching. – Miscreant Jun 30 '17 at 03:13
4

i have an idea to solve this issue. when you create the iframe give a name/id to the iframe. .

and, in the script inside iframe send the message as object which will look like

window.parent.postMessage({"height" : $('.widget').outerHeight(), "frmname" : window.name}, '*');

in the parent listener,

eventer(messageEvent, function(e) {`enter code here`
    $(e.data.frmname).height(e.data.height);
}, false);
aha
  • 89
  • 2
3

The following works for me cross-origin:

window.addEventListener('message', function (event) {
  if (event.data.size) {
    Array.prototype.forEach.call(document.getElementsByTagName('iframe'), function (element) {
      if (element.contentWindow === event.source) {
        element.style.height = `${event.data.size.height}px`;
      }
    });
  }
}, false);

Tested in Chromium 64 and Firefox 59.

Mitar
  • 5,851
  • 2
  • 44
  • 68
1

If the source iframe is nested in more than one parent iframe, then you will need to recurse over the window.frames property of each iframe and compare it against the messageEvent#source property.

For example, if the message is generated by iframe#level3 of this Dom.

<iframe Id=level1>
   <iframe Id=level2>
       <iframe Id=level3 />
   </iframe>
</iframe>

You should be able to find the index of the ancestor iframe in the current window using.

FindMe = event.source
FrameIndex = find(window)
frames[FrameIndex].frameElement ==     getElByTagName(iframe)[FrameIndex] 

function find(target){
    for (i=0; I< target.frames.length; i ++)
       if(target.frames[i] == FindMe ||   find(target.frames[i]))
           return i
    return false 
}

Its important to note that

The Window.frames property is not restricted by cross domain policy

This technique will work regardless of how deeply nested the source iframe happens to be

window.frames is a collection of window objects not iframe elements.

access to the propetties s of the memebers of the window.frames collection is resttictred by Same Origin (I.e you may not be able to access the frameElement or location properties of window.frames[i]

-2

The event should also have a property "source" that can be compared to the iframes "contentWindow" property.

deCastongrene
  • 312
  • 2
  • 3