In JavaScript, once I've received a 'message' event, is there a way to find out which frame in the DOM model has initiated it? This would be helpful when debugging a large web application where a particular message could have come anyone of 15-20 frames. The message event has a source property, but if the frame is cross-domain, it's not accessible:
Since I know these things vary from browser to browser, I'm asking specifically about IE11.
I found a way that actually works even when cross-domain - I add a DOM element by evaluating it in the Add Watch window. Then I search the DOM tree for that element, and figure out the frame in this way.
For example, this code works:
var foo_btn = document.createElement("BUTTON"); var foo_t = document.createTextNode("FOOBAR FOOBAR"); foo_btn.appendChild(foo_t); document.body.appendChild(foo_btn);
You just click Add Watch and paste it, and then after it executes, you can search for FOOBAR FOOBAR in the DOM tree.
Related
I am developing a Chrome extension with a manifest that, for now, enables access to all hosts. The background script injects content scripts into all frames. After the DOM is loaded, the content script in the top page/frame begins to walk the DOM tree. When the walker encounters an iframe, it needs to message the specific content script associated with that iframe's window (possibly cross-origin) to begin it's work and includes some serialized data with this message. The parent window suspends execution and waits for the child to complete it's walk and send a message back that it is done along with serialized data. The parent then continues its work. I have tried two approaches to this problem:
frameElement.contentWindow.postMessage: this works most of the time, but not always. Sometimes the message is never received by the content script message event listener associated with the iframe window. I have not been able to confirm the cause but I think it is listeners attached before my listener calling event.stopImmediatePropagation(). For example, on the yahoo home page (https://www.yahoo.com), when posting a message to my content script associated with iframe source https://s.yimg.com/rq/darla/2-9-9/html/r-sf.html, the message is never received. This is an ad-related iframe. Maybe the blocking of messages is intentional. There is no error when the message is posted and I use a targetOrigin of "*".
chrome.runtime.sendMessage: I can send a message to the background page but cannot figure out how to tell the background page to which frame to relay the message. The parent window content script does not know the chrome extension frameId associated with the child frame element it encountered in the DOM walk. So it cannot tell the background page how to direct the message.
For point 2, I have tried two techniques that I found here on stackoverflow:
Using concept described in this question: In parent window, determine iframe's position in the window.frames array and post a message to the background page with this index. The background page posts a message to all frames with the desired index in the message data. Only the iframe that finds it's window object position in the window.parent.frames array matches the index received from the message proceeds with it's walk. This works OK but is vulnerable to changes in the window.frames array during the asynchronous messaging process (if an iframe is deleted after message is sent, the index value may no longer match the desired frame).
Instead of the index value from point 1, use frameElement.name in the parent window. With same messaging technique, send name to child iframe for comparison to its window.name value. I believe window.name gets it's value from the frameElement.name at the time of the iframe element creation. However, since I don't control the frame element creation, the name attribute is often an empty string and can't be relied on to uniquely match iframe elements to their windows.
Is there a way for me to reliably send a message to a content script associated with an iframe element found in walking a DOM tree?
When you call chrome.runtime.sendMessage from a content script, the second parameter of the chrome.runtime.onMessage listener ("sender") includes the properties url and frameId.
You can send a message (from an extension page, e.g. the background page) to a specific frame using chrome.tabs.sendMessage with the given frameId.
If you want to know the list of all frames (and their frame IDs) at any time, use the chrome.webNavigation.getAllFrames. If you do that, then you can construct a tree of the frames in a tab, and then send this information to all frames for further processing.
Reliable postMessage / onMessage
frameElement.contentWindow.postMessage: this works most of the time, but not always. Sometimes the message is never received by the content script message event listener associated with the iframe window. I have not been able to confirm the cause but I think it is listeners attached before my listener calling event.stopImmediatePropagation()
This can be countered by running your script at "run_at":"document_start" and immediately register the message event listener. Then your handler will always be called first and the page cannot cancel it via event.stopImmediatePropagation(). However, do not blindly trust the information from other frames and always verify the message (e.g. by communicating with the other frames via the background page).
Combining both approaches
The first method offers a secure way to exchange data between frames, but does not offer a general way to link the frame to a specific DOM element.
The second method allows you to target a specific (i)frame element, but any web page can do that and therefore the method on its own is not reliable.
By combining both, you get a secure communication channel that is linked to a DOM element.
This is a basic example that applies the above methods to communicate between frames A and B:
Content script in A:
Send a message to the background page (e.g. a message including the index of frame B).
Background page:
Receives the message from A.
Generate a random nonce, say R (crypto.getRandomValues).
Store a mapping from R to frameId (and optionally other information that was included in the message from A).
Call the response callback with this random value.
Content script in A:
Receive R from the background page.
Calls postMessage on frame B and pass R.
Content script in B:
Receive R from A.
Send a message to the background page to retrieve the frameId (and optionally other information from A).
Note: For a rock-solid application, you need to account for the fact that the frame is removed during any of those steps. If you neglect the asynchronous nature of this process, you may leave your application in an inconsistent state.
tl;dr
My answer will describe a CORS-proof solution in the specific case when the child frame has user focus. In many usecases, usually the frame we want to interact with has focus.
In 2022, Firefox and Safari already have CORS-proof standard API for this, so if you're targeting them, consider using the standard API instead.
My solution does not use window.postMessage or cryptographic random values.
Prerequisites
The frame - to which you are sending a message - needs be a 'valid frame'. A 'valid frame' is:
a frame which has user focus, OR
parent of another valid frame
For simplicity of discussion, I'll assume:
we have <all_urls> host permission
we are working inside only one tab
Glossary
A frame tree is such that each frame has a unique parent but can have multiple children. The depth of a frame is:
Zero if it is the root document, OR
One plus depth of its parent frame
Procedure
Step 1. Track depth of each frame
tl;dr: When a frame loads, we record its depth in the frame tree.
I will assume your content script (CS) already injects itself into each iframe on the page. As soon as it is injected, the CS needs to report its own frame depth to the background page (BG). Using this information, BG will maintain the list of frame IDs at each depth level.
CS can get its own depth by using recursive algorithm similar to the one described here
BG can access sender.frameId in the onMessage listener to correctly get the frame Id.
BG now has a list reportedFrameDepths (for example) where reportedFrameDepths[depth] is a list/set of all frameIds at that depth.
Step 2. Check which child frame is focused
tl;dr: Given a frame, we can find which one of its child frame is focused.
We can enumerate all candidate children of this frame by checking reportedFrameDepths[depth + 1], where depth is the frame depth of this frame. Only one of the frames in this list should have user focus.
The focused child will have non-null document.activeElement value, and document.hasFocus() will be true. We need to check the latter as in certain cases (for example, mail.google.com), document.activeElement is set to a non-focused element (<body>) for many frames.
So we can send a message to all the candidate frames (specify { frameId } in options field of tabs.sendMessage) and get a boolean response from them if they have focus. The one frameId that responds true should be the intended focused child frame.
Step 3. Repeat step 2 recursively.
If you can find the focused child A of a given frame, you can also find the focused child B of that focused child A.
Repeating step 2 starting from root document will lead you to the deepest focused child. The recursion stops when there is no further focused child.
This is the end, you now have the frame ID of the deepest focused child. You can now send a message directly to this frame.
Gotchas
This is not a trivial solution to implement, due to:
the amount of asynchronicity. There is a long messaging chain across multiple CS and the BG. Ensure your code can handle if the messaging chain is interrupted midway due to some other parts of the code crashing.
Tabs and frames can reload, navigate away or destroy themselves. Make sure your implementation handles these cases. Especially be wary of caching or data stores as they can become obsolete.
That said, I have implemented the solution for a similar usecase and it works reliably and fast enough (extra overhead I observed is less than 5ms). The exact implementation will vary depending on your product's needs, and the above explanation should serve as a good reference.
I'm trying to port one of my Firefox extensions to support Electrolysis (e10s). My extension grabs some page data and puts it on the clipboard via a context-menu item that the user can click. Based on the message manager documentation, there are 3 types of message managers available:
Global
Window
Browser
Since my add-on is context specific, the last one seems like the one I want to use. The problem is that I don't fully know when to load the frame script. A simplified version of my context menu item's action handling code looks like this:
onContext: function() {
let browserMM = gBrowser.selectedBrowser.messageManager;
browserMM.loadFrameScript("chrome://myaddon/content/frame-script.js", true);
browserMM.sendAsyncMessage("myaddon#myaddon.com:get-page-info", json);
}
Loading the frame script here seemed like the best idea to me since (a) the frame script isn't guaranteed to get used on every page and (b) I figured that frame scripts are loaded once and only once per <browser>. The second theory isn't correct it seems; each time I call loadFrameScript, a new copy gets loaded. Even load-protection logic (i.e. only creating the frame script functions if they don't already exist) doesn't seem to fix the problem.
So, my problem is that each time the context menu item is accessed, a new copy of the frame script gets loaded. And since my frame script adds a message listener, I get duplicate messages on subsequent calls of the context menu item.
When should I load browser frame scripts? Loading it once on add-on initialization doesn't seem to work well, since it only loads on the first <browser> (I want this code to execute when asked for by any subsequent <browser>). But loading it on demand appears to duplicate things.
Are there other strategies I'm missing here?
Even load-protection logic (i.e. only creating the frame script functions if they don't already exist)
Frame scripts are a bit tricky, scripts for each tab share a global object but have a separate scope, akin to being evaluated inside their own function block. So if you add it multiple times to a tab then each gets evaluated in a separate scope.
Instead you might want to track the browser objects that already have your frame script attached with a WeakMap. Although I think there also is some property to enumerate the loaded frame scripts.
Loading it once on add-on initialization doesn't seem to work well
If you want that, then use the global message manager and attach a delayed frame script, that'll get attached to all current and future tabs. Of course that will consume more memory than just attaching it to tabs that really need it.
browserMM.loadFrameScript("chrome://myaddon/content/frame-script.js", true);
You don't really need to set the delayed flag to true if you run it on a specific browser, that only makes sense for broadcasting message manager which may get additional children in the future.
It is possible to move deeper into the DOM tree using the .frame JSON Wire Protocol call but I haven't been able to figure out a way to move upward through the DOM tree.
module.exports = {
"Enter and exit iframes in tree" : function(browser){
browser
//Currently focus is at top level.
.frame('iframeOne')
//Focus is now enters inner frame iframeTwo
.frame('iframeTwo')
//Attempt to move to frame directly
.frame('iframeOne')
//Selenium Error 'no such frame'
//Attempt to move focus up to iframeOne by searching from root.
.element('id', 'iframeOne', function(e){
browser.frame(e.value);
}
//Selenium Error 'no such element'
}
There is a JSON Wire Protocol call frame/parent that is able to move up to the parent element but it is not currently supported by NightWatchJS. Any suggestions would be appreciated.
In current binary releases, you need to use the switchToDefaultContent command. In Java this manifests itself as driver.switchTo().defaultContent(). This will take you to the top of the frame hierarchy, and you can navigate back down the tree to the frame you need.
The "navigate to parent frame" wire protocol is brand new. On the order of days, at the time of this writing. No released server implementation at the time of this writing exists that understands that wire protocol endpoint. The Firefox driver has had it implemented only in the last day or so. Work hasn't started yet on the IE driver. It's not yet implemented in any language binding except Java, and that's only in the source tree; it hasn't been released in a binary form yet. If you're patient, it will be available to you in the future (no available timeline so don't ask), just not yet.
I was able to get around this problem by resetting the focus back up to the top level element and crawling down again.
.frame(null)
.frame('iframeOne')
It's a workable solution.
I am building a firefox extension that creates several hidden browser elements.
I would like to addProgressListener() to handle onLocationChange for the page that I load. However, my handler does not always get called.
More specifically, here's what I'm doing:
Create a browser element, without setting its src property
Attach it to another element
Add a progress listener listening for onLocationChange to the browser element
Call loadURIWithFlags() with the desired url and post data
I expect the handler to be called every time after 4, but sometimes it does not (it seems to get stuck on the same pages though).
Interestingly, if I wrap 3 and 4 inside a setTimeout(..., 5000); it works every time.
I've also tried shuffling some of the steps around, but it did not have any effect.
The bigger picture: I would like to be reliably notified when browser's contentDocument is that of the newly loaded page (after redirects). Is there a better way to do this?
Update: I've since opened a bug on mozilla's bug tracker with a minimal xulrunner app displaying this behavior, in case anybody wants to take a closer look: https://bugzilla.mozilla.org/show_bug.cgi?id=941414
In my experience developing with Firefox, I've found in some cases the initialization code for various elements acts as if it were asynchronous. In other words, when you're done executing
var newBrowser = window.document.createElement('browser');
newBrowser.setAttribute('flex', '1');
newBrowser.setAttribute('type', 'content');
cacheFrame.insertBefore(newBrowser, null);
, your browser may not actually be ready yet. When you add the delay, things have time to initialize, so they work fine. Additionally, when you do things like dynamically creating browser elements, you're likely doing something that very few have tried before. In other words, this sounds like a bug in Firefox, and probably one that will not get much attention.
You say you're using onLocationChange so that you can know when to add a load listener. I'm going to guess that you're adding the load listener to the contentDocument since you mentioned it. What you can do instead is add the load listener to the browser itself, much like you would with an iframe. If I replace
newBrowser.addProgressListener(listener);
with
newBrowser.addEventListener("load", function(e) {
console.log('got here! ' + e.target.contentDocument.location.href);
}, false);
then I receive notifications for each browser.
I found a JavaScript error in a script that I'm developing. I whittled it down to a pair of fairly concise pages that still produce the error:
http://troy.onespot.com/static/4505/index.html
http://troy.onespot.com/static/4505/iframe.html
If you load the first page in Internet Explorer 8 (or probably 6 or 7, for that matter), give it about half a second to run the script, then click the "Google" link inside of the <iframe>, you should see an error message appear:
http://skitch.com/troywarr/dui21/ie8-error
(You may need to uncheck Tools > Internet Options > Advanced > "Disable script debugging (Internet Explorer)" and check "Display a notification about every script error" two lines below to see the error messages.)
Starting the debugger shows the beautifully insightful message "Object required":
http://skitch.com/troywarr/dui26/ie8-debugging
The culprit is the line:
target = event_object.target || event_object.srcElement;
I think that's valid code - at least it works in Firefox. My best guess is that there is an issue with trying to access the Event Object of one frame from another - vaguely similar to why you can't rely on instanceof to detect arrays if they were created in a different window/frame (search for "context" at http://javascript.crockford.com/remedial.html if that didn't make sense).
Does that sound like a valid theory? If so, what can I do to fix this? If at all possible, I need to preserve the same general code structure/functionality:
There is a link inside an <iframe> on a page.
A script in the <iframe> calls a function on the parent page, which attaches an event handler to the element in the <iframe> with the specified id attribute.
Clicking that <iframe>d link triggers the event, which calls a function on the parent page, passing the Event Object by default.
The function on the parent page determines information about the clicked element (the <a>) from the Event Object in a cross-browser-compatible way.
I would also like to continue using event delegation, and keep all of the functions in the parent document, just calling them with arguments from the <iframe>d document. However, if you have any suggestions for alternative approaches, I'd love to hear them.
Thanks in advance for any help! Please let me know if you need any more explanation about my requirements or what I'm trying to do - I'm hoping that there's just a better way to access or pass the Event Object that I'm not aware of - an "Oh, yeah, you just need to do it like this" kind of solution. I hope that's not wishful thinking. ;-)
Easily fixed. You need the event object from the iframe's window. Store the iframe's window object next to where you store its document:
var iframe_win = document.getElementsByTagName('iframe')[0].contentWindow;
... and update the line that gets hold of the event object in your event handler function:
event_object = event_object || iframe_win.event;