Here is a very simple XMLHttpRequest utility function:
/**
* Use XMLHttpRequest to fetch the contents of the specified URL using
* an HTTP GET request. When the response arrives, pass it (as plain
* text) to the specified callback function.
*/
HTTP.getText = function(url, callback) {
var request = HTTP.newRequest();
request.onreadystatechange = function() {
if (request.readyState == 4) {
if (request.status == 200) callback(request.responseText);
}
}
request.open("GET", url);
request.send(null);
};
This function assumes the presence of a HTTP.newRequest() function to abstract away the differences in creating an XMLHttpRequest object in IE and other browsers. (You've seen a function like this in every Ajax utility library you've looked at...)
Since XMLHttpRequest uses ActiveX in IE, it is unavailable if the user has disabled ActiveX for security reasons. In this situation, we can try to fall back on other means of scripting HTTP. Both <iframe> and <script> tags have src attributes that can generate HTTP GET requests when dynamically set. Let's start there and see if we can use these tags to create an analog to the HTTP.getText() method shown above. (Full compatibility with XMLHttpRequest is not possible with these techniques, however.)
IFrames are commonly cited as the tag of choice for "dynamic scripting" without XMLHttpRequest. The basic idea is that you create a new iframe, set its onload property, and then set its src property to the URL that you want to fetch. The problem is that we want to fetch a plain text resource (such as a JavaScript file, which can be treated as plain text) but the <iframe> tag wants to display an HTML document. Plain text loaded into an iframe (in Firefox at least) ends up stuffed into a <pre> tag, and has its angle brackets and ampersands replaced with their corresponding HTML entities.
It should be possible to extract the contents of the <pre> tag and replace the entities with the characters they replaced. I decided to go a different route, however, and encode the plain text to be transferred in a CDATA section of an XML file. Here is a PHP script that does this:
<?php
header("Content-Type: text/xml");
echo '<?xml version="1.0"?>';
echo '<data>';
echo '<![CDATA[';
readfile($_GET["url"]);
echo ']]>';
echo '</data>';
?>
With this script, it was pretty easy to write a getText() function that obtained the contents of a specified URL as plain text. In Firefox, anyway. Things got very complicated in IE, and I simply could not make it work on this platform. (Remember that the motivation is to find an alternative to XMLHttpRequest to use when ActiveX is disabled in IE).
Kae Verens has made this work but I'm not sure what he's doing differently. I don't think he's using an XML CDATA section, and I didn't study his server-side code well enough to figure out how he encodes the data for transfer to the client.
Anyway, after failing with the <iframe> approach, I decided to try using a <script> tag instead. Again, we use a server-side script to encode the contents of the URL we want to transfer. This time, we encode the contents as a (possibly very long) JavaScript string, and pass that string to a JavaScript function. One pleasant side effect of this approach is that there is no need for an onload or onreadystatechange event handler. When the script loads, it executes, and we've encoded the callback call into the script, so the callback just gets called.
Enough talk. Here is the server-side PHP script. It should be in a file name jsquoter.php.
<?php
header("Content-Type: text/javascript");
// Get arguments from the URL
$func = $_GET["func"]; // The function to invoke in our js code
$filename = $_GET["url"]; // The file or URL to pass to the func
$lines = file($filename); // Get the lines of the file
$text = implode("", $lines); // Concatenate into a string
// Escape quotes and newlines
$escaped = str_replace(array("'", "\"", "\n", "\r"),
array("\\'", "\\\"", "\\n", "\\r"),
$text);
// Output everything as a single JavaScript function call
echo "$func('$escaped');"
?>
And here is the client-side function that uses the PHP script above. We've succeeded in duplicating the API of the HTTP.getText() function that we began with:
HTTP.getTextWithScript = function(url, callback) {
// Create a new script element and add it to the document
var script = document.createElement("script");
document.body.appendChild(script);
// Get a unique function name
var funcname = "func" + HTTP.getTextWithScript.counter++;
// Define a function with that name, using this function as a
// convenient namespace. The script generated on the server will
// invoke this function
HTTP.getTextWithScript[funcname] = function(text) {
// Delete the script tag we created
document.body.removeChild(script);
// Delete this function
delete HTTP.getTextWithScript[funcname];
// And invoke the callback
callback(text);
}
// Encode the url we want to fetch, and the name of the function
// as arguments to the jsquoter.php server-side script. Set the src
// property of the script tag to fetch the URL
script.src = "jsquoter.php" + "?url=" + encodeURIComponent(url) +
"&func=" +
encodeURIComponent("HTTP.getTextWithScript." + funcname);
}
// We use this to generate unique function callback names in case there
// is more than one request pending at a time.
HTTP.getTextWithScript.counter = 0;
This code has been lightly tested in Firefox and IE6, (with PHP 4.1 on the server). It works for me! Feel free to use it yourself, and please post any suggestions or bug reports in the comments.
One final note: in some PHP configurations, the url argument we pass to jsquoter.php has to be a local file. In many configurations, however, it can be an arbitrary URL. In this case, the PHP script acts as a proxy server and defeats the "same-origin" restriction that prevents XMLHttpRequest from making requests to any server other than the one from which the script was loaded. This should be safe as long as your web application is in control of the URLs that are submitted to jsquoter.php. But don't let the user specify the URL to be loaded in this way, or you'll probably be opening up a security hole.




The reason why an iframe with XML does not work well in IE is that IE uses XSLT to do pretty printing of the XML and therefore the DOM is actually the HTML DOM of the XML pretty print. For iframes it is better to just use HTML and HTML encode the text inside a pre element
Thanks Erik,
I figured out what IE was doing with XML today when I saw that my XML document had a HEAD and a BODY!
I never tried an HTML encoding, since I thought that I'd then have to decode it again on the client-side. I'd forgotten that < , >, and & are automatically expanded in the DOM, so I shouldn't have worried about this.
I think I prefer my based solution anyway, since it avoids the need for an onload handler, which seems to be quite broken in IE.
Lovely IE...take a look at onreadystatechange:
http://msdn.microsoft.com/workshop/author/dhtml/reference/events/onreadystatechange.asp?frame=true
Tom,
IE 5 and before do not fire onload or onreadystatechange at all for iframes. See:
http://support.microsoft.com/kb/q239638/
I think this is why Kae Veren's script polls the readyState property rather than using onreadystatechange.
(I haven't tested my based solution on IE 5 or 5.5, however, so they may not work on those platforms, either!)
That's it exactly, David. I tried a load of ways of figuring out when exactly the frame was finished loading, but there was no one event-firing way that was usable, so I set up a polling function for it.
As for how I made it work on the server-side; I used the amazingly simple Sajax library as a base, and adapted it - here's the code I came up with: http://verens.com/demos/Sajax.phps
Ah, sorry, didn't realize you were trying to support IE 5 at all...my bad :)
Why not use the same idea as your <script> tag but with the iframe: the URL can point to a valid HTML page that just contains a script wich contains your data. When the page load, your script is executed and from there you have many possibilities to retrieve the data - for exemple the script could directly execute your callback.
I have used a similar method in a real time dhtml chat system that didn't need to refresh the page to display new messages: the application used an iframe to a "never ending" server side HTML page that the server simply fed with snippet of scripts upon new events in the server queue. Those scripts , when executed in the browser, posted the event in the application queue and from there redered in the chat window. Of course that technique required one permanent HTTP connection to the server per user.