Ext version tested:
Adapter used:
css used:
- default ext-all.css
- optionally examples.css from Ext examples to make test case look nice, but this is not a UI issue.
Browser versions tested against:
- Chrome 10.0.648.204
- IE9, IE7
- FF 3.6.15 (firebug 1.7.1 installed)
- Opera 11.01
- Safari 5.0.3 (7533.19.4)
Operating System:
- Windows 7 64-bit for all browsers except IE7
- Windows Server 2003 for IE7
Description:
When you make an Ext.Ajax.request with a form containing a file input to be uploaded, or manually set the isUpload option to true, rather than doing a proper Ajax request Ext submits the form in the standard HTML way to a dynamically generated hidden <iframe>. The json response body is then read out of the iframe to create a faked-up Ajax response. A problem arises if the page making the upload Ajax request has changed its document.domain property, e.g. a page at home.example.com includes resources from static.example.com which it wishes to manipulate with javascript without violating the browser's same-origin-policy, so both set their document.domain to "example.com". If home.example.com then makes an upload Ajax request to a url on the home.example.com server, the iframe into which the response is written will have its document.domain as "home.example.com". Thus when the ExtJS code within Ajax.request on the home.example.com page tries to extract the document body from the iframe, it will be blocked by the same-origin-policy and the response passed to the callback functions will incorrectly have empty responseText.
Test Case:
Code:
<html>
<head>
<title>Upload Form to iFrame with changed document.domain</title>
<!-- TODO update these paths for your environment, they are currently based on this file being in the Ext examples/form directory -->
<!-- ** CSS ** -->
<!-- base library -->
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css"/>
<!-- overrides to base library -->
<!-- page specific, just to make it pretty, no big deal if missing -->
<link rel="stylesheet" type="text/css" href="../shared/examples.css" />
<!-- ** Javascript ** -->
<!-- base library -->
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all.js"></script>
</head>
<body>
<h1>File Upload to iFrame with changed document.domain</h1>
<p>The example demonstrates how shortening the document.domain causes the response from file-upload-style Ajax requests to get lost.</p>
<p>To run this test you need to deploy it to a real web server rather than run from local file system (so that document.domain is not blank) and the server should have a urls <b>welcomeAjax</b> and <b>welcomeUpload</b>.</p>
<p>The current value of document.domain is: </p>
<div id="domainText"></div>
<br>
<div id="trimBtn"></div>
<br>
<div id="welcomeForm"></div>
<script type="text/javascript">
Ext.BLANK_IMAGE_URL = '../../resources/images/default/s.gif';
Ext.onReady(function() {
var domainText = new Ext.form.TextField({
readOnly: true,
renderTo: 'domainText',
width: 300
});
function displayDomain() {
domainText.setValue(document.domain);
}
displayDomain();
new Ext.Button({
renderTo: 'trimBtn',
text: 'Trim Domain',
handler: function() {
document.domain = document.domain.substr(document.domain.indexOf('.') + 1);
displayDomain();
}
});
var welcomeForm = new Ext.form.FormPanel({
bodyStyle: 'padding: 5px;',
buttonAlign:'center',
renderTo: 'welcomeForm',
title: 'Welcome Form',
width: 300,
height: 100,
items: [{
fieldLabel: 'Name',
name: 'name',
xtype: 'textfield'
}],
buttons: [
{
text: 'Submit with Normal Ajax',
handler: function() {
makeRequest();
}
}, {
text: 'Submit with File Upload',
handler: function() {
makeRequest(true);
}
}
]
});
function makeRequest(upload) {
Ext.Ajax.request({
url: upload ? '../welcomeUpload' : '../welcomeAjax', // TODO Adjust these for your server
form: welcomeForm.getForm().getEl(),
isUpload: upload,
success: function(response) {
if (!response) {
Ext.Msg.alert('Success?', 'No response');
}
else if (!response.responseText) {
Ext.Msg.alert('Success?', 'Response but empty responseText, this is the symptom of the domain bug!');
}
else {
Ext.Msg.alert('Success', Ext.decode(response.responseText).msg);
}
},
failure: function() {
Ext.Msg.alert('Error', 'Generic fail');
}
});
}
});
</script>
</body>
</html>
As this issue relates to document.domain, you need to deploy this file to a real webserver, preferably with a sub-domain as in "foo.bar.com" so the document.domain can be set to "bar.com". If you have just a domain "foo.com", setting the domain to "com" is dodgy.
The webserver also needs 2 urls to provide json respones, presented below in nodeJS code, adapt this to whatever server technology you use.
Code:
// In our nodeJS framework this is how we make controller methods
exports.handlers = {
welcomeAjax: {
handler: function(params, response) {
response.setHeader('Content-Type', 'application/json');
// Write json and end
response.end(JSON.stringify({msg: 'Welcome ' + params.name}));
}
},
welcomeUpload: {
handler: function(params, response) {
// Note content type must be set to html for file uploads, not doing so is a common cause of errors and thus forum posts.
response.setHeader('Content-Type', 'text/html');
response.end(JSON.stringify({msg: 'Welcome ' + params.name}));
}
}
};
Steps to reproduce the problem:
- Deploy the test harness html to your web server and create the server side urls to serve the json response.
- On the page, type you name in the Name text box.
- 1. Click Submit with Normal Ajax
- 2. Click Submit with File Upload
- 3. Click Trim Domain
- 4. Click Submit with Normal Ajax
- 5. Click Submit with File Upload
The result that was expected:
- 1. and 2. each cause a Welcome message box to appear
- After 3. you see in the domain textbox that the domain has been shortened, e.g. from foo.bar.com to bar.com
- 4. and 5. each cause a Welcome message box to appear
The result that occurs instead:
- 1. and 2. each cause a Welcome message box to appear
- After 3. you see in the domain textbox that the domain has been shortened, e.g. from foo.bar.com to bar.com
- 4. causes a Welcome message box to appear
- 5. causes a message box with "Response but empty responseText, this is the symptom of the domain bug!" to appear. If you look in the Chrome debug console you will see an error message like "Unsafe JavaScript attempt to access frame with URL http://foo.bar.com/welcomeUpload from frame with URL http://foo.bar.com/upload-domain.html. Domains, protocols and ports must match."
Debugging already done:
- I have determined that it is the browser's same-origin-policy that means the parent page cannot access the document within the dynamic iframe that contains the response body. Inspecting the html of the page (using firebug, Chrome dev tools or equivalent) I can see the iframe and it does contain the response json.
- The problem occurs on line 6543 of ext-all-debug.js in the doFormUpload function
Code:
doc = frame.contentWindow.document || frame.contentDocument || WINDOW.frames[id].document;
frame.contentWindow evaluates ok, but when trying to access the document property of the resulting object generates the access denied error. The 3 OR clauses here leads to 3 copies of the error in the console
Possible fix:
I tried hacking the Ext source code to set the document.domain of the <iframe> when it is constructed to be the same as that of the page creating it, but failed to do so. I have managed to workaround this problem by sending the document.domain as an extra parameter on all file upload requests, and then changing the server-side code to inject a script tag which sets the document.domain of the response html document containing the json to that specified in the parameter. But this is rather unpleasant and the ideal solution would be if somehow Ext JS could change the iframe's domain.
My workaround comprises the following changes:
We have a wrapper around Ext.Ajax.request which, similar to within Ext, can turn a params based request into a form upload one (but with added custom functionality)
Code:
// items are hidden form field config corresponding to the request parameters
formPanel = new Ext.form.FormPanel({
hidden: true,
items: items,
renderTo: Ext.getBody() // Have to add it to the DOM somewhere to get the <FORM>
});
args.isUpload = true;
args.form = formPanel.getForm().getEl();
Ext.Ajax.request(args);
To the items is added an extra domain parameter:
Code:
items.push({
name: '__domain',
value: document.domain,
xtype: 'hidden'
});
Then server-side the welcomeUpload handler changes to inject a script tag in the header (so the body is still just the json and can be used to reconstruct the fake XHR response):
Code:
welcomeUpload: {
handler: function(params, response) {
// Note content type must be set to html for file uploads, not doing so is a common cause of errors and thus forum posts.
response.setHeader('Content-Type', 'text/html');
response.write('<html><head><script type="text/javascript">document.domain = "' + params.__domain + '";</script></head><body>');
response.write(JSON.stringify({msg: 'Welcome ' + params.name}));
response.end('</body></html>');
}
}
With this workaround applied, all 4 clicks in the test case cause the welcome message to appear.