Take Command with AJAX
Do you want to build more dynamic, responsive, desktop-like Web applications like Gmail and Google Maps? Then this article is for you. It guides you through the AJAX basics and through the process of building a simple AJAX application. That application is named WebConsole and, in essence, it s a browser interface for executing system commands for which you d usually need shell access.
AJAX is a newly coined term for two powerful JavaScript features that have been around for years, but were overlooked by many Web developers until recently, when applications such as Google Maps, Flickr and A9.com s Search Results hit the streets. If you re new to AJAX and XMLHTTP, keep reading.
In this article, I ll explain the creation of one simple, reusable JavaScript function for making HTTP requests. Then, I ll apply that function in the creation of a simple application.
The article is not a tutorial on JPSpan, Sajax, or any other AJAX libraries. Instead, it aims to give you more hands-on information about making HTTP requests, so that you re in a better position when evaluating such libraries or deciding to go on your own.
A Simple HTTP Request Example
Let s first revise the flow of making an HTTP request in JavaScript, and handling the response. This is just a quick example to refresh your memory. For all the spicy details, see SitePoint s introductory article, AJAX: Usable Interactivity with Remote Scripting.
Our JavaScript will request an HTML document, test.html, which contains the text "I m a test." We ll then alert() the contents of the test.html file.
<script type="text/javascript" language="javascript">
var http_request = false;
function makeRequest(url) {
if (window.XMLHttpRequest) { // Mozilla, Safari,...
http_request = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
http_request = new ActiveXObject("Microsoft.XMLHTTP");
}
http_request.onreadystatechange = alertContents;
http_request.open( GET , url, true);
http_request.send(null);
}
function alertContents() {
if (http_request.readyState == 4) {
if (http_request.status == 200) {
alert(http_request.responseText);
} else {
alert( There was a problem with the request. );
}
}
}
</script>
<a href="#" onclick="makeRequest( test.html )">Make a request</a>
Here s how this example works:
- The user clicks the link "Make a request" in the browser.
- This calls the
makeRequest() function with a parameter: the name of an HTML file in the same directory. In this case, it s test.html.
- The request is made.
- The
onreadystatechange event fires and the execution is passed to alertContents().
alertContents() checks if the response was received and, if it s OK, then alert()s the contents of the test.html file.
Test the example for yourself, and view the test file.
The Problem
The above example worked just fine, but there s one thing we need to improve before we re ready for primetime. The improvement is to code a reusable request function that handles all the boring and repetitive object creation and request/response stuff, while leaving the presentational part to other functions, which are request-agnostic and deal with the result only, regardless of its source.
In the example above, we needed a global variable, http_request, that was accessible by both the makeRequest() and alertContents() functions, which is not very good in terms of reusability. Ideally, makeRequest() should perform the request and alertContents() should just present the result. So, makeRequest() should not be aware of the presence of alertContents() -- it should not have it coded in its body -- while, at the same time, alertContents() should simply alert() some text, without knowing where it came from or having to deal with request status and readyState details.
Here s the code for our reusable request function; some details follow.
function makeHttpRequest(url, callback_function, return_xml)
{
var http_request = false;
if (window.XMLHttpRequest) { // Mozilla, Safari,...
http_request = new XMLHttpRequest();
if (http_request.overrideMimeType) {
http_request.overrideMimeType( text/xml );
}
} else if (window.ActiveXObject) { // IE
try {
http_request = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
try {
http_request = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {}
}
}
if (!http_request) {
alert( Unfortunatelly you browser doesn t support this feature. );
return false;
}
http_request.onreadystatechange = function() {
if (http_request.readyState == 4) {
if (http_request.status == 200) {
if (return_xml) {
eval(callback_function + (http_request.responseXML) );
} else {
eval(callback_function + (http_request.responseText) );
}
} else {
alert( There was a problem with the request.(Code: + http_request.status + ) );
}
}
}
http_request.open( GET , url, true);
http_request.send(null);
}
This function receives three parameters:
- The URL to get
- The function to call when the response is received
- Flag if the result should be passed as XMLDocument or as plain text to the callback function
This function relies on two JavaScript capabilities in order to wrap and isolate the request object nicely; the first is the ability to define new functions (called anonymous functions) on the fly, like this:
http_request.onreadystatechange = function() {...}
The other trick is the usage of the eval() function, which allows you to treat any string as JavaScript code, and to evaluate (execute) it. In this code snippet, eval() is used to construct a call to the response-handling function whose name was passed as parameter.
The function is inspired by this tutorial on jibbering.com and this onlamp.com tutorial.
You can easily make the function even more reusable by allowing the HTTP requests method and any query string to be passed as parameters to the function and then used in calls to open() and send() methods. This will also allow you to make POST requests in addition to the GETs it was originally intended to perform.
Another addition to the function might be the handling of response codes other than 200, which could be handy if you want to be more specific and take appropriate actions depending on the type of the success/error code returned.
The Simple Example Revisited
Now let s redo the previous example in which the contents of a test.html file were alert()ed. This time, by employing our shiny new reusable request function, the revised versions of the two functions used will be much simpler:
function alertContents(text) {
alert(text);
}
function makeRequest(url) {
makeHttpRequest(url, alertContents );
}
As you can see, alertContents() is simply presentational: there are no states, readyStates or HTTP requests flying around whatsoever.
Since these functions are now just one-liners, we can, in fact, get rid of them entirely, and change the function call instead. So the whole example will become:
<a href="#" onclick="makeHttpRequest( test.html , alert )" >Make a request</a>
Yes, it s that easy! View the example and full source code (available through our old friend View Source).
Our Project: The WebConsole Application
Knowing the AJAX basics, and armed with a reusable way of making requests, let s go deeper, to create a little something that can actually be used in real life.
The application we ll create will allow you to execute any shell command on your Web server, whether it s Windows- or Linux-based. We ll even put in a little CSS effort in an attempt to make the app feel more like a console window.
Interface-wise, we have one scrollable <div> that contains the results of the commands executed so far, and one textbox into which we type the commands to be executed. They both have a black background and gray courier font. Here s a screenshot.
<V:IMAGEDATA o:href="http://i2.sitepoint.com/graphics/web_console_screens hot.gif" src="file:///C:DOCUME~1Square1LOCALS~1Tempmsohtml1
The HTML
Here s the HTML <body> part of the application:
<body>
<form onsubmit="return false">
<div
class="wc-results"
id="result">
Welcome to the WebConsole!
<br />
:->
</div>
<input
onkeyup="keyEvent(event)"
class="wc-command"
id="command"
type="text" />
</form>
</body>
That s it: a <div> that gets updated, and an <input> into which we can type commands. It s a nice, clean interface, with no <iframe> wrestling, no page reloads -- none of that! A JavaScript function, keyEvent(), is attached to the onkeyup event of the input box.
The CSS
The style sheet wc.css defines the styles for the result <div> and the command <input>:
.wc-results {
overflow: auto;
margin: 0px;
padding: 5px;
font-family: courier;
color: gray;
background-color: black;
height: 400px;
}
.wc-results pre {
display: inline;
}
.wc-command {
margin: 0px;
font-family: courier;
color: gray;
background-color: black;
width: 100%;
}
We make the <div> that shows the command execution results scrollable by setting its overflow property to auto. We also change the <pre> tag display property to inline (block is its default).
The Server-side Code
Our application will make requests to a server-side script (exec.php), which receives a command through the GET parameter command . This script simply checks that the command appears in the allowed list (you can edit this list to allow more commands), executes the command, and prints the result. The command is executed with the help of the native PHP function shell_exec(). PHP is the selected language here, but it should be relatively easy to implement this functionality using your preferred server-side language.
<?php
if(strcmp(strtoupper(substr(PHP_OS, 0, 3)), "WIN") == 0) {
// Windows commands
$allowed_commands = array ( cd , dir , more wc.css , more test.html , copy test.html test.txt , more test.txt , del test.txt );
} else {
// Linux, Mac OS X, etc. commands
$allowed_commands = array ( ls -la , ls , ls -l , less wc.css , less test.html , touch test.txt , cp test.html test.txt , less test.txt , rm test.txt );
}
if (!empty($_GET[ command ]) && in_array($_GET[ command ], $allowed_commands)) {
echo shell_exec($_GET[ command ]);
} else {
echo "This demo version lets you execute shell commands only from a predefined list: ";
echo implode(" ", $allowed_commands);
}
?>
WARNING!
The $allowed_commands array restricts the commands that users can execute through the console. You can add as many commands as you like to the array, but beware that any additional commands will really be executed on your Web server: adding format c:, apachectl stop or rm –Rf, for example, is not recommended!
The JavaScript
The flow of the JavaScript code in the application is as follows:
- The
keyEvent() function is attached to the onkeyup event of the input field and is called every time a key is pressed and released.
keyEvent() checks if the key with code 13 (this is the Enter/Return key) is pressed.
- If Enter is pressed, the URL for the request is constructed like so:
exec.php?command=the-command-entered-by-the-user
- The URL is passed to our reusable
makeHttpRequest() function. Also, the name of the function -- printResult() -- is provided as a parameter to makeHttpRequest().
- After a successful server response,
printResult() is called.
printResult() updates the result <div>, scrolls down the <div>, and clears the command text box to make room for the next command to be typed.
Here s the body of the keyEvent() function:
function keyEvent(event)
{
switch(event.keyCode){
case 13:
var the_shell_command = document.getElementById( command ).value;
if (the_shell_command) {
var the_url = exec.php?command= + escape(the_shell_command);
makeHttpRequest(the_url, printResult );
}
break;
default:
break;
}
}
Because we didn t pass TRUE as a third parameter to makeHttpRequest(), the text response (not XML) will be passed to printResult().
Next, let s take a look at the function that will update the result <div>. There is one quick way to update that <div>, and it s to use the innerHTML property of the <div> element, like this:
document.getElementById( result ).innerHTML += And-the-result-goes-here ;
But the use of innerHTML to dynamically update the HTML code of Web pages is discouraged, because it treats the HTML code as a string, while modern Web design thinking prefers to treat the code of an HTML page as an XML tree of nodes, accessible through the DOM methods and properties. The DOM is the path we ll now take, in order to update our <div>.
Here s the function; below are some notes on how it works:
function printResult(result_string)
{
var result_div = document.getElementById( result );
var result_array = result_string.split( );
var new_command = document.getElementById( command ).value;
result_div.appendChild(document.createTextNode(new_com mand));
result_div.appendChild(document.createElement( br ));
for (var line_index in result_array) {
var result_wrap = document.createElement( pre )
line = document.createTextNode(result_array[line_index]);
result_wrap.appendChild(line);
result_div.appendChild(result_wrap);
result_div.appendChild(document.createElement( br ));
}
result_div.appendChild(document.createTextNode( :-> ));
result_div.scrollTop = result_div.scrollHeight;
document.getElementById( command ).value = ;
}
This function:
- Adds the command that was entered in the
<input> to the result <div> by creating a new text node and adding it to the document tree
- Adds the result of the command execution. This is done by splitting the result into lines and adding each line to the document tree, while wrapping each of these lines in
<pre> tags to preserve the spacing. We need to split the result because it may contain several lines (imagine the result if a ls -la (or dir on Windows) was executed)
- Adds a new cursor-like textNode (
:->)
- Scrolls down the
<div>, using the (non-W3C-standard but supported by modern browsers) scrollTop and scrollHeight properties
- Clears the command
<input> so that the next command can be entered
And there you go! This was the last piece of the puzzle. What we have now is a working AJAX application built from scratch.
A Little Extra
If you were curious enough to look at the source code of the previous example, you might have noticed that there is a bit more to this app than what we ve discussed so far. The little extra is not really AJAX-related, but it makes the application feel more like a command prompt. The functionality in question involves the use of the up and down arrow keys to access the history of the commands used in a session.
Let s say you executed ls -la , then ls . If you hit the up arrow key, the command <input> will be pre-filled with the command that was used last, that is, ls . Hit the up arrow again and the command input will show ls -la . Hit the down arrow key. You get ls again, as you moved forward through the history of commands. Try it out yourself.
The implementation of this feature is not difficult. We just need an array that will store all the commands executed so far:
var commands_history = new Array();
...and an array pointer (an integer) that remembers where we were, keying arrows up and down:
var history_pointer;
Those two variables are global, which allows them to keep their values after keyEvent() has done its job. Here s the listing of the keyEvent() function. The lines that deal with the history functionality are shown in bold.
function keyEvent(event)
{
switch(event.keyCode){
case 13:
var the_shell_command = document.getElementById( command ).value;
if (the_shell_command) {
commands_history[commands_history.length] = the_shell_command;
history_pointer = commands_history.length;
var the_url = exec.php?command= + escape(the_shell_command);
makeHttpRequest(the_url, printResult );
}
break;
case 38: // this is the arrow up
if (history_pointer > 0) {
history_pointer--;
document.getElementById( command ).value = commands_history[history_pointer];
}
break;
case 40: // this is the arrow down
if (history_pointer < commands_history.length - 1 ) {
history_pointer++;
document.getElementById( command ).value = commands_history[history_pointer];
}
break;
default:
break;
}
}
Some notes on how the function works to provide the commands history:
- When we hit Enter (key code 13) and we make a request, the executed command is added to the
commands_history array, and the array pointer is reset to the new length of the array.
- When hitting the up arrow (key code 38), which means "go back", we decrement
history_pointer and pre-fill the command <input> with the previous command in the history list.
- Hitting the down arrow increments the pointer by one, and we see the next command.
Working with XML
So far, we haven t discussed how to request and use XML documents -- the X in AJAX! We were using the responseText property of the XMLHTTP object. Requesting the document is no different than what we ve already seen: we just need to instruct our reusable request function to return responseXML, as opposed to responseText. We do that by setting the third parameter to TRUE:
makeHttpRequest(the_url, printResult , true);
Then, we need to change our exec.php script to return valid XML, instead of plain text. Here s the source code of the new script (exec_xml.php):
<?php
header( Content-Type: text/xml );
echo <?xml version="1.0" ?> . " ";
echo <exec> . " ";
echo <command> . htmlentities($_GET[ command ]) . </command> . " ";
echo <result> ;
if (!empty($_GET[ command ])) {
$result = array();
exec($_GET[ command ], $result);
if (!empty($result)) {
$result = array_map( htmlentities , $result);
echo <line> ;
echo implode($result, "</line> <line>");
echo </line> ;
} else {
echo <line>No output from this command. A syntax error?</line> ;
}
}
echo </result> . " ";
echo </exec> ;
?>
This way, if we execute the command ls test.html , the new server-side script will return the following:
<?xml version="1.0" ?>
<exec>
<command>ls test.html</command>
<result>
<line>test.html</line>
</result>
</exec>
If we execute a command that returns more lines (like ls -la ), every line in the response will be wrapped in <line> tags.
We ll navigate the above XML document using the JavaScript DOM functions, in order to process the <result> and display it in our result <div>.
Here s the body of the new printResult() function:
function printResult(xmldoc)
{
var result_div = document.getElementById( result );
var result_collection = xmldoc.getElementsByTagName( line );
var new_command = xmldoc.getElementsByTagName( command ).item(0).firstChild.nodeValue;
result_div.appendChild(document.createTextNode(new_com mand));
result_div.appendChild(document.createElement( br ));
var number_of_items = result_collection.length;
for (var i = 0; i < number_of_items; i++) {
if (result_collection.item(i).hasChildNodes()) {
var result_wrap = document.createElement( pre );
var line = document.createTextNode(result_collection.item(i).firstChild .nodeValue);
result_wrap.appendChild(line);
result_div.appendChild(result_wrap);
}
result_div.appendChild(document.createElement( br ));
}
result_div.appendChild(document.createTextNode( :-> ));
result_div.scrollTop = result_div.scrollHeight;
document.getElementById( command ).value = ;
}
In order to update the result <div> with the data from the XML document, we follow the procedure:
- Access a node from the source XML.
- Get its value.
- Create a new node.
- Append it to the
<div> target tree.
As you see in the code xmldoc.getElementsByTagName, ( command ) is used, and it returns a collection (a list) of all <command> nodes. In our case, there s only one such element. We access its value with the following:
xmldoc.getElementsByTagName( command ).item(0).firstChild.nodeValue;
We take the node value, and create a new text node to append to the <div>, like this:
var new_command = xmldoc.getElementsByTagName( command ).item(0).firstChild.nodeValue;
result_div.appendChild(document.createTextNode(new_command)) ;
We do the same with the result tag of the XML document. First, we get all <line>s:
var result_collection = xmldoc.getElementsByTagName( line );
Then, we loop through each element in the result_collection. Again, we wrap each line of result in <pre> tags.
As you see, working with the XMLDocument is not much more difficult than working with the plain text response. You can test the XML version of the WebConsole yourself.
Conclusion
We ve come to the end of our AJAX example. You know the basics, you saw the action, and you re armed with enough knowledge to start experimenting yourself. You can warm up by modifying and playing around with this article s code -- all of it s included in the downloadable code archive -- then move to projects of your own.
These are exciting times: the face of the Web is undergoing big changes, thanks to remote scripting. Google, Amazon and Flickr are just a few big names already using these techniques. You can also be a part of the change!
Security Reminder!
Don t forget to protect the directory on your server in which you ll install the examples from this article. A quick .htaccess protection will do. Leaving this application accessible to strangers can have devastating results. It s pretty powerful: it allows the user to execute any command, including, but not limited to, deleting everything on your Web server!
.