Article

Take Command with Ajax

Page: 1 2 3 Next

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 <input> where we type the commands to be executed. They both have a black background and gray courier font. Here’s a screenshot.

WebConsole interface screenshot

The HTML

Here’s the HTML part of the application:

<form action="exec.php" method="get" id="console-form">  
 <div  
   class="console"  
   id="result">  
   Welcome to the WebConsole!  
   <br />  
   :-&gt;  
 </div>  
 <input  
   class="console"  
   name="command"  
   id="command"  
   type="text" />  
</form>

That’s it: a <div> that gets updated with the results of the command being executed, 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!

The CSS

The style sheet webconsole.css defines the styles for the result <div> and the command <input>:

.console {  
   margin: 0px;  
   font-family: courier;  
   color: gray;  
   background-color: black;  
}  
#result {  
   overflow: auto;  
   padding: 5px;  
   height: 400px;  
}  
#result pre {  
   display: inline;  
}  
#command {  
   width: 100%;  
   border: 1px solid white;  
}

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). There’s also the reusable .console class to make everything look “consoley:” gray monospaced font on black background.

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 used 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 webconsole.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 webconsole.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:\n";  
  echo implode("\n", $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 first step in the JavaScript code is to define a namespace: a glorified label for what is essentially nothing more than just an empty object:

var WebConsole = {};

All other variables and functions we need will be defined as properties of this object. This allows us to keep the global namespace clean and the code self-contained.

The flow of the JavaScript code in the application is as follows:

  1. The WebConsole.keyEvent() function is attached to the onkeyup event of the input field, and is called every time a key is pressed and released.

  2. WebConsole.keyEvent() checks if the key with code 13 is pressed (this is the Enter/Return key).

  3. If Enter is pressed, the URL for the request is constructed like so: exec.php?command=the-command-entered-by-the-user

  4. The URL is passed to our reusable makeHttpRequest() function. Also, the name of the callback function—WebConsole.printResult—is provided as a parameter to makeHttpRequest().

  5. After a successful server response, WebConsole.printResult() is called.

  6. WebConsole.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 what the body of the keyEvent() function could look like:

WebConsole.keyEvent = function(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, WebConsole.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 that’s to use the innerHTML property of the element, like so:

document.getElementById(‘result’).innerHTML += ‘the-result-goes-here’;

But the use of innerHTML to dynamically update web pages is discouraged, because it treats the HTML code as a string, while modern web design thinking prefers to treat the page as a document containing 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 it are some notes on how it works:

WebConsole.printResult = function(result_string)  
{  
 var result_div = document.getElementById(‘result’);  
 var result_array = result_string.split(‘\n’);  
 
 var new_command = document.getElementById(‘command’).value;  
 result_div.appendChild(document.createTextNode(new_command));  
 result_div.appendChild(document.createElement(‘br’));  
 
 var result_wrap, line_index, line;  
 
 for (line_index in result_array) {  
   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
  • displays 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 text node (:->)
  • scrolls down the <div>, using the scrollTop and scrollHeight properties (non-W3C-standard but supported by modern browsers)
  • clears the command <input> so that the next command can be entered

The last task in the JavaScript is to handle the events:

  • form submits are simply “muted” so that there’s no page refresh
  • the method WebConsole.keyEvent() is attached to the keyup event of the input where commands are typed.

document.getElementById(‘console-form’).onsubmit = function(){  
   return false;  
};  
document.getElementById(‘command’).onkeyup = function(e){  
   if (!e && window.event) {  
       e = window.event;  
   }  
   WebConsole.keyEvent(e);  
};

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 may 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 prefilled with the command that was used last; that is, ‘ls’. Hit the up-arrow key again and the command input will show ‘ls -la’. Hit the down-arrow key. You reach ‘ls’ again as you move 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:

WebConsole.commands_history = [];

...and an array pointer (an integer) that remembers where we were:

WebConsole.history_pointer = 0;

Here’s the listing of the WebConsole.keyEvent() function. The lines that deal with the history functionality are shown in bold.

WebConsole.keyEvent = function(event)  
{  
 var the_url, the_shell_command;  
 switch(event.keyCode){  
   case 13:  
     the_shell_command = document.getElementById(‘command’).value;  
     if (the_shell_command) {  
       this.commands_history[this.commands_history.length] = the_shell_command;  
       this.history_pointer = this.commands_history.length;
 
       the_url = ‘exec.php?command=‘ + escape(the_shell_command);  
       makeHttpRequest(the_url, WebConsole.printResult);  
     }  
     break;  
 
   case 38: // this is the arrow up  
     if (this.history_pointer > 0) {  
       this.history_pointer--;  
       document.getElementById(‘command’).value = this.commands_history[this.history_pointer];  
     }  
     break;  
   case 40: // this is the arrow down  
     if (this.history_pointer < this.commands_history.length - 1 ) {  
       this.history_pointer++;  
       document.getElementById(‘command’).value = this.commands_history[this.history_pointer];  
     }  
     break;
 
   default:  
     break;  
 }  
};

Here are 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 prefill 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.

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links