The postings on this site are the contributor's and don’t necessarily represent IBM’s positions, strategies or opinions.
March 22nd, 2009 Alex Moffat Posted in AJAX, GWT | 7 Comments »
Cross-site scripting using script elements and JSONP is supported by lots of services, such the Twitter REST APIs and the Google Data APIs. In an earlier post I described one possible cross-site scripting implementation in GWT. However, this technique is restricted to the GET method.
In fact, when I wrote that post, I thought it wasn’t possible to do cross-site POST calls. Ray Cromwell pointed out that this isn’t true and had already demonstrated how to do it using GWT in a post last year based on some work in dojo. This is my explanation of the principles behind the the technique, because I had to puzzle out how it worked.
As with the JSONP/script approach the issues are, how do you get access to the data returned by the server, and how do you know when the server has responded?
To access the data the name property of the window object is used. This is an “old” property, in internet years, and is available in at least IE6 and later browsers. You can set the window.name with JavaScript. Most importantly for this technique the value set persists across page reloads, it is not affected by the document loaded in the window, and it can hold a lot of data. Exploiting this to store session data on the client, instead of using cookies, was first done by Thomas Frank, as far as I can determine, in his session vars implementation.
To know when the server has responded you use the onLoad event on an iframe.
The cross-site post solution combines the window.name property with an iframe in the following way.
Step 0. You need a server method that will return data in the required format. For the JSONP approach you needed JSON wrapped in a function call. For the window.name technique you have to return an html page containing a script element that will set the window.name property to the data you want. For example
<html>
<script type="text/javascript">
window.name="server data";
</script>
</html>
Of course in a real case “server data” could be JSON or GWT RPC encoded objects.
Step 1. Create an iframe element. Give it a name. This is where the data from the server will end up. When the data arrives the script will execute and the iframe.contentWindow.name property will be set. In the example we’ll end up with iframe.contentWindow.name set to “server data”.
Step 2. Create a form element. Add input elements for the data you want to send to the server. Set the action to point to the service in the domain you want to communicate with and method appropriately. Set the target property for the form to the name of the iframe element created in step 1. Now when the form is submitted the data will be returned to the iframe.
Step 3. Attach a function to the onLoad event on the iframe. This will be called when the data is returned from the server, after it has been parsed by the browser. So when this function executes, the script in the returned html page will have run and the name property of the iframe window will be set to the data we want.
Unfortunately, because the page in the iframe was loaded from a different domain than the containing page code associated with the iframe can’t communicate with its containing document and vice versa. The value of window.name is not accessible from the containing page, including the iframe onLoad function. However, because the window.name property isn’t affected by document loading, if we load a new document into the iframe the data in window.name remains. Therefore, in the onLoad function set the document.location for the iframe to point to something, such as a 1 pixel image, from the domain of the containing page. This way we switch the iframe back to the same domain as the containing page.
Step 4. When the onLoad function fires for the second time window.name will still contain the data set by the page from server. Now though the domain of the iframe is the same as the domain of the containing page and it’s possible for the onLoad function to call a function in the containing page either just to notify it that the data is available, because the containing page can now read iframe.contentWindow.name, or to pass the data directly.
Ray Cromwell’s GWT implementation of this technique in GWT is quite elegant. GWT provides a FormPanel class and this already uses an iframe to receive the results of the form submission. FormPanel uses FormPanelImpl to contain the JSNI methods it needs for the native JavaScript parts of its implementation and deferred binding to provide a separate IE implementation. It’s only two methods in these two implementations that need minor modifications, and these modification can be done by providing two new implementations, WindowNameFormPanelImpl and WindowNameFormPanelImplIE6, and using deferred binding to select these classes at compile time in preference to FormPanelImpl and FormPanelImplIE6.
The first method is getContents. Only one change needed here, getting the data from the name property instead of from the document. This method is the same for IE and non-IE browsers, but, because of the class hierarchy, it has to be overridden in the same way in both the IE and non-IE WindowNameFormPanelImpl classes. I’m showing deleted code with strikethroughs and added code with green italics.
public native String getContents(Element iframe) /*-{
try {
// Make sure the iframe's window & document are loaded.
if (!iframe.contentWindow || !iframe.contentWindow.document)
return null;
// Get the body's entire inner HTML.
return iframe.contentWindow.document.body.innerHTML;
// Get the contents from the window.name property.
return iframe.contentWindow.name;
} catch (e) {
return null;
}
}-*/;
The other method to override is hookEvents. This adds an onload event handler to the iframe and an onsubmit event handler to the form. Shown below is the non IE implementation. In the onload function we have to detect the “first” load of data, this is coming from the cross-site server, and use this to set the iframe back to the original domain, which will trigger onLoad for a second time, at which point the name property will be available. The added code uses the __sameDomainRestored property to detect the first load and loads the clear.cache.gif image to reset the domain. The onsubmit function sets the __sameDomainRestored property to false when the form is submitted.
public native void hookEvents(Element iframe, Element form,
FormPanelImplHost listener) /*-{
if (iframe) {
iframe.onload = function() {
// If there is no __formAction yet, this is a spurious onload
// generated when the iframe is first added to the DOM.
if (!iframe.__formAction)
return;
if (!iframe.__sameDomainRestored) {
iframe.__sameDomainRestored = true;
iframe.contentWindow.location =
@com.google.gwt.core.client.GWT::getModuleBaseURL()() +
"clear.cache.gif";
return;
}
listener.@com.google.gwt.user.client.ui.impl.FormPanelImplHost::onFrameLoad()();
};
}
form.onsubmit = function() {
// Hang on to the form's action url, needed in the
// onload/onreadystatechange handler.
if (iframe) {
iframe.__formAction = form.action;
iframe.__sameDomainRestored = false;
}
return listener.@com.google.gwt.user.client.ui.impl.FormPanelImplHost::onFormSubmit()();
};
}-*/;
The IE implementation is very similar. The only difference is that the onload function is replaced by an onreadystatechange function. I’ve shown the changes to this function below.
iframe.onreadystatechange = function() {
// If there is no __formAction yet, this is a spurious onreadystatechange
// generated when the iframe is first added to the DOM.
if (!iframe.__formAction)
return;
if (iframe.readyState == 'complete') {
// If the iframe's contentWindow has not navigated to the expected action
// url, then it must be an error, so we ignore it.
if (!iframe.__sameDomainRestored) {
iframe.__sameDomainRestored = true;
iframe.contentWindow.location =
@com.google.gwt.core.client.GWT::getModuleBaseURL()() +
"clear.cache.gif";
return;
}
listener.@com.google.gwt.user.client.ui.impl.FormPanelImplHost::onFrameLoad()();
}
};
March 26th, 2009 at 2:26 pm
Hey Alex, great write-up, and a better explanation than mine! A cool hack to do would be to modify the ServiceInterfaceGeneratorProxy/ProxyGenerator and RemoteServiceProxy class to use this technique instead of XHR, and create a subclass of RemoteServiceServlet to deal with incoming RPC that wants the output as a window.name assignment.
March 28th, 2009 at 4:42 am
Great solution. Do you think it is possible to have a method like
crosspost(url,callback,jsonData)
Is there any easy way to convert JSON object into form? Or some other way to make it work with above solution?
I want to export an API for end users who don’t have to deal with all the complexity involved in above solution.
March 29th, 2009 at 9:42 pm
@Ray Thanks. That would be an interesting hack. I’d need a motivating example before I tried to tackle it though
@Rajesh It might be possible. The thing is though, to get cross site posting to work you need the cooperation of the server, it’s got to be returning a script that sets window.name. If you have enough control to get it to do that I would think it would be easier to just provide the same functionality via a GET method. Cross site file upload would require the POST technique I think but why not try to do everything else with GET which lets you use the much simpler script technique with JSONP. Server cooperation is still needed but the client side is perhaps more easily understood.
June 14th, 2009 at 10:23 pm
Do you know if this works with GWT 1.6? I can get the POST to make the post call to a secondary domain.
I can not seem to get the POST results back into any usable state. I see the response back in FF using firebug; however, the window.name always returns the iframe name set by GWT, which is an incremented name like “FormPanel_XX”.
I’m trying to get a simple sample working and any pointers would be much appreciated. Here is a thread with a full description of what I am trying: http://groups.google.com/group/Google-Web-Toolkit/browse_frm/thread/139a99baf2382340#
Hopefully it is something simple that I have overlooked on my part.
Thanks,
Chris….
June 15th, 2009 at 9:58 am
Unfortunately I don’t know the answer. Ray Cromwell could probably give you a more useful response. I see from the thread that it works in IE6 so my first thought, that it’s targeting the iframe instead of the window, doesn’t seem likely.
June 24th, 2009 at 12:34 pm
There is now a new library available, easyXSS, at http://code.google.com/p/easyxss that in very few lines enables you to send data and call methods across the domain boundry.
Take a look!
June 7th, 2010 at 5:12 am
Nice post Alex!
I noticed that history management in IE and Safari is affected by this technique. IE will cooperate if you use contentWindow.location.replace instead of directly assigning the old domain URL to it. Safari will insist on creating a new history entry for each form submission