Looking at the title of this chapter, you probably said to yourself, “JavaScript? What does that have to do with CGI programming or Perl?” It’s true that JavaScript is not Perl, and it cannot be used to write CGI scripts.[9] However, in order to develop powerful web applications we need to learn much more than CGI itself. Therefore, our discussion has already covered HTTP and HTML forms and will later cover email and SQL. JavaScript is yet another tool that, although not fundamental to creating CGI scripts, can help us create better web applications.
In this chapter, we will focus on three specific applications of JavaScript: validating user input in forms; generating semiautonomous clients; and bookmarklets. As we will soon see, all three of these examples use JavaScript on the client side but still rely on CGI scripts on the server side.
This chapter is not intended to be an introduction to JavaScript. Since many web developers learn HTML and JavaScript before turning to Perl and CGI, we will assume you’ve had some exposure to JavaScript already. If you haven’t, or if you are interested in learning more, you may wish to refer to JavaScript: The Definitive Guide by David Flanagan (O’Reilly & Associates, Inc.).
Before we get started, let’s discuss the background of JavaScript. As we said, we’ll skip the introduction to JavaScript programming, but we should clear up possible confusions about what we mean when we refer to JavaScript and how JavaScript relates to similar technologies.
JavaScript was originally developed for Netscape Navigator 2.0. JavaScript has very little to do with Java despite the similarity in names. The languages were developed independently, and JavaScript was originally called LiveScript. However Sun Microsystems (the creator of Java) and Netscape struck a deal, and LiveScript was renamed to JavaScript shortly before its release. Unfortunately, this single marketing decision has confused many who believe that Java and JavaScript are more similar than they are.
Microsoft later created their own JavaScript implementation for Internet Explorer 3.0, which they called JScript . Initially, JScript was mostly compatible with JavaScript, but then Netscape and Microsoft developed their languages in different directions. The dynamic behavior provided in the latest versions of these languages is now very different.
Fortunately, there have been efforts to standardize these languages via ECMAScript and DOM. ECMAScript is an ECMA standard that defines the syntax and structure of the language that JScript and JavaScript will become. ECMAScript itself is not specific to the Web and is not directly useful as a language because it doesn’t do anything; it only defines a few very basic objects. That’s where the Document Object Model (DOM) comes in. The DOM is a separate standard being developed by the World Wide Web Consortium to define the objects used with HTML and XML documents without respect to a particular programming language.
The end result of these efforts is that JavaScript and JScript should one day adopt both the ECMAScript standard as well as the DOM standard. They will then share a uniform structure and a common model for interacting with documents. At this point they will both become compatible and we can write client-side scripting code that will work across all browsers that support this standard.
Despite the distinction between JavaScript and JScript, most people use the term JavaScript in reference to any implementation of JavaScript or JScript, regardless of browser; we will also use the term JavaScript in this manner.
The biggest issue with JavaScript is the problem we just discussed: browser compatibility. This is not something we typically need to worry about with CGI scripts, which execute on the web server. JavaScript executes in the user’s browser, so in order for our code to execute, the browser needs to support JavaScript, JavaScript needs to be enabled (some users turn it off), and the particular implementation of JavaScript in the browser needs to be compatible with our code.
You must decide for yourself whether the benefits that you gain from using JavaScript outweigh these requirements that it places upon the user. Many sites compromise by using JavaScript to provide enhanced functionality to those users who have it, but without restricting access to those users who do not. Most of our examples in this chapter will follow this model. We will also avoid newer language features and confine ourselves to JavaScript 1.1, which is largely compatible between the different browsers that support JavaScript.
Probably the most popular use for JavaScript with web applications is to improve HTML forms. Standard HTML forms aren’t very smart. They simply accept input and pass it on to the web server where all the processing must occur. With JavaScript, however, we can do much more on the client side. JavaScript can validate input before it is sent to the server. Forms can also dynamically react to user input and update fields in order to provide immediate feedback to the user; a dynamic form can often substitute for multiple static forms.
The benefit JavaScript provides for the server is that it shifts some work that might otherwise be done on the server to the client, and it reduces the number of server requests. The benefit JavaScript provides to the user is that it provides immediate feedback without a delay while the browser fetches a new page.
When you create an HTML form, you generally expect the user to fill it out in a particular way. There are numerous types of restrictions a form may have. For example, some of the fields may only accept numbers while others may only accept dates, some fields may only accept a certain range of entries, some fields may be required, and some combinations of fields may not permitted. All of these examples must be handled by only two types of checks: the first is to validate each element user’s input as the data is entered; the second is to perform the validation when the form is submitted.
Checking a form element when the user enters a value is most effective for validating the format or range of a particular element. For example, if a field only accepts numbers, you can verify that the user did not enter any non-numeric characters.
To perform this check, we use the
onChange
event handler. This handler supports
the following form elements:
Text, TextArea, Password, File
Upload, and Select. For each of these elements, we can
register an onChange
handler and assign it code
to execute when the element changes. We register it simply by adding
it as an attribute to the HTML tag that creates the element. For
example:
<INPUT TYPE="text" name="age" onChange="checkAge( this );">
This runs the function
checkAge
and passes it a reference to
itself via this
. checkAge
looks like this:
function checkAge ( element ) { if ( element.value != parseInt( element.value ) || element.value < 1 || element.value > 150 ) { alert( "Please enter a number between 1 and 150 for age." ); element.focus( ); return false; } return true; }
This function checks that the age entered is an integer and between 1 and 150 (sorry if you happen to be 152, but we have to draw the line somewhere).
If checkAge
determines that the input is
invalid, it displays an alert asking the user to enter the value
again (Figure 7.1), and moves the cursor back to
the age field via element.focus( )
. It then returns true or false
depending on whether the check was successful or not. This
isn’t necessary, but it does help us if we later decide to
string multiple function calls together as you’ll see later in
Example 7.2.
Note that we don’t need to call a function to handle
onChange
. We can assign multiple statements
directly to the
onChange
handler. However, it’s often much
easier to work with HTML documents when the JavaScript is kept
together as much as possible, and functions help us to do this. They
also allow us to share code when we have multiple form elements that
require the same validation. For functions that you use often, you
can go a step further and place these in a JavaScript file that you
can include in multiple HTML files. We’ll see an example of
this in Figure 7.2.
The other way that we
can perform
data validation is to do so just before the form is submitted. This
is the best time to check whether required fields have been filled or
to perform checks that involve dependencies between multiple
elements. We perform this check with JavaScript’s
onSubmit
handler.
onSubmit works like the
onChange
handler except that it is added as an
attribute of the <FORM> tag:
<FORM METHOD="POST" ACTION="/cgi/register.cgi" onSubmit="return checkForm(this);">
There’s another difference you may notice. The
onSubmit
handler returns the value of the code
that it calls. If the onSubmit
handler returns
false, it cancels the submission of the form after the handler code
has run. In any other case, the form submission continues. Return
values have no effect on the onChange
handler.
Here is the function that checks our form:
function checkForm ( form ) { if ( form["age"].value == "" ) { alert( "Please enter your age." ); return false; } return true; }
This example simply verifies that a value was entered for age.
Remember, our onChange
handler is not enough to
do this because it is only run when the value for age changes. If the
user never fills in a value for age, the
onChange
handler will never be called. This is
why we check for required values with onSubmit
.
Let’s look at a complete example. It seems that more and more web sites want users to register and provide lots of personal information in order to use their web site. We’ll create a slightly exaggerated version of a registration form (Figure 7.2).
Note that this form applies only to United States residents. In practice, Internet users come from around the world, so you must be flexible with your validation to accommodate the various international formats for phone numbers, postal codes, etc. However, since the purpose of this example is to demonstrate validation, we’ll restrict the formats to one set that can be easily validated. The required formats for phone numbers and social security numbers are shown. In addition, the zip code is a five-digit postal code.
The HTML is shown in Example 7.1.
Example 7-1. input_validation.html
<html> <head> <title>User Registration</title> <script src="/js-lib/formLib.js"></script> <script><!-- function validateForm ( form ) { requiredText = new Array( "name", "address", "city", "zip", "home_phone", "work_phone", "age", "social_security", "maiden_name" ); requiredSelect = new Array( "state", "education" ); requiredRadio = new Array( "gender" ); return requireValues ( form, requiredText ) && requireSelects( form, requiredSelect ) && requireRadios ( form, requiredRadio ) && checkProblems ( ); } // --> </script> </head> <body bgcolor="#ffffff"> <h2>User Registration Form</h2> <p>Hi, in order for you to access our site, we'd like first to get as much personal information as we can from you in order to sell to other companies. You don't mind, do you? Great! Then please fill this out as accurately as possible.</p> <p>Note this form is for U.S. residents only. Others should use the <a href="intl_registration.html">International Registration Form</a>.</p> <hr> <form method="POST" action="/cgi/register.cgi" onSubmit="return checkForm( this )"> <table border=0> <tr><td> Name: </td><td> <input type="text" name="name" size="30" maxlength="30"> </td></tr> <tr><td> Address: </td><td> <input type="text" name="address" size="40" maxlength="50"> </td></tr> <tr><td> City: </td><td> <input type="text" name="city" size="20" maxlength="20"> </td></tr> <tr><td> State: </td><td> <select name="state" size="1"> <option value="">Please Choose a State</option> <option value="AL">Alabama</option> <option value="AK">Alaska</option> <option value="AZ">Arizona</option> . . . <option value="WY">Wyoming</option> </select> </td></tr> <tr><td> Zip Code: </td><td> <input type="text" name="zip" size="5" maxlength="5" onChange="checkZip( this );"> </td></tr> <tr><td> Home Phone Number: </td><td> <input type="text" name="home_phone" size="12" maxlength="12" onChange="checkPhone( this );"> <i>(please use this format: 800-555-1212)</i> </td></tr> <tr><td> Work Phone Number: </td><td> <input type="text" name="work_phone" size="12" maxlength="12" onChange="checkPhone( this );"> <i>(please use this format: 800-555-1212)</i> </td></tr> <tr><td> Social Security Number (US residents only): </td><td> <input type="text" name="social_security" size="11" maxlength="11" onChange="checkSSN( this );"> <i>(please use this format: 123-45-6789)</i> </td></tr> <tr><td> Mother's Maiden Name: </td><td> <input type="text" name="maiden_name" size="20" maxlength="20"> </td></tr> <tr><td> Age: </td><td> <input type="text" name="age" size="3" maxlength="3" onChange="checkAge( this );"> </td></tr> <tr><td> Gender: </td><td> <input type="radio" name="gender" value="male"> Male <input type="radio" name="gender" value="female"> Female </td></tr> <tr><td> Highest Education: </td><td> <select name="education" size="1"> <option value="">Please Choose a Category</option> <option value="grade">Grade School</option> <option value="high">High School Graduate (or GED)</option> <option value="college">Some College</option> <option value="junior">Technical or Junior College Degree</option> <option value="bachelors">Four Year College Degree</option> <option value="graduate">Post Graduate Degree</option> </select> </td></tr> <tr> <td colspan=2 align=right> <input type="submit"> </td></tr> </table> </form> </body> </html>
You don’t see much JavaScript here because most of it is in a separate file that is included with the following pair of tags on line 5:
<script src="/js-lib/formLib.js" ></script>
The contents of formLib.js are shown in Example 7.2.
Example 7-2. formLib.js
// formLib.js // Common functions used with forms // // We use this as a hash to track those elements validated on a per element // basis that have formatting problems validate = new Object( ); // Takes a value, checks if it's an integer, and returns true or false function isInteger ( value ) { return ( value == parseInt( value ) ); } // Takes a value and a range, checks if the value is in the range, and // returns true or false function inRange ( value, low, high ) { return ( !( value < low ) && value <= high ); } // Checks values against formats such as '#####' or '###-##-####' function checkFormat( value, format ) { var formatOkay = true; if ( value.length != format.length ) { return false; } for ( var i = 0; i < format.length; i++ ) { if ( format.charAt(i) == '#' && ! isInteger( value.charAt(i) ) ) { return false; } else if ( format.charAt(i) != '#' && format.charAt(i) != value.charAt(i) ) { return false; } } return true; } // Takes a form and an array of element names; verifies that each has a value function requireValues ( form, requiredValues ) { for ( var i = 0; i < requiredValues.length; i++ ) { element = requiredText[i]; if ( form[element].value == "" ) { alert( "Please enter a value for " + element + "." ); return false; } } return true; } // Takes a form and an array of element names; verifies that each has an // option selected (other than the first; assumes that the first option in // each select menu contains instructions) function requireSelects ( form, requiredSelect ) { for ( var i = 0; i < requiredSelect.length; i++ ) { element = requiredSelect[i]; if ( form[element].selectedIndex <= 0 ) { alert( "Please select a value for " + element + "." ); return false; } } return true; } // Takes a form and an array of element names; verifies that each has a // value checked function requireRadios ( form, requiredRadio ) { for ( var i = 0; i < requiredRadio.length; i++ ) { element = requiredRadio[i]; isChecked = false; for ( j = 0; j < form[element].length; j++ ) { if ( form[element][j].checked ) { isChecked = true; } } if ( ! isChecked ) { alert( "Please choose a " + form[element][0].name + "." ); return false; } } return true; } // Verify there are no uncorrected formatting problems with elements // validated on a per element basis function checkProblems ( ) { for ( element in validate ) { if ( ! validate[element] ) { alert( "Please correct the format of " + element + "." ); return false; } } return true; } // Verifies that the value of the provided element has ##### format function checkZip ( element ) { if ( ! checkFormat( element.value, "#####" ) ) { alert( "Please enter a five digit zip code." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element has ###-###-#### format function checkPhone ( element ) { if ( ! checkFormat( element.value, "###-###-####" ) ) { alert( "Please enter " + element.name + " in 800-555-1212 " + "format." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element has ###-##-#### format function checkSSN ( element ) { if ( ! checkFormat( element.value, "###-##-####" ) ) { alert( "Please enter your Social Security Number in " + "123-45-6789 format." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; } // Verifies that the value of the provided element is an integer between 1 and 150 function checkAge ( element ) { if ( ! isInteger( element.value ) || ! inRange( element.value, 1, 150 ) ) { alert( "Please enter a number between 1 and 150 for age." ); element.focus( ); validate[element.name] = false; } else { validate[element.name] = true; } return validate[element.name]; }
We use both types of validation in this example: validating
elements as they are entered and
validating the form as a whole when it is submitted. We create a
validate
object that we use like a Perl hash.
Whenever we validate an element, we add the name of this element to
the validate
object and set it to true or false
depending on whether the element has the correct format. When the
form is
submitted,
we later loop over each element in validate
to
determine if there are any elements that had formatting problems and
were not fixed.
The functions that handle
specific field validation are checkZip
,
checkPhone
, checkSSN
, and
checkAge
. They are called by the
onChange
handler for each of these form
elements and the functions appear at the bottom of
formLib.js. Each of these functions use the more
general functions isInteger
,
isRange
, or checkFormat
to
check the formatting of the element they are validating.
isInteger
and isRange
are
simple checks that return whether a value is an integer or whether it
is within a particular numeric range.
checkFormat
takes a value as well as a
string containing a format to check the value against. The structure
of our format string is quite simple: a pound symbol represents a
numeric digit and any other character represents itself. Of course,
in Perl we could easily do checks like this with a regular
expression. For example, we could match social security number with
/^ddd-dd-dddd$/
. Fortunately, JavaScript
1.2 also supports regular expressions. Unfortunately, there
are still many browsers on the Internet that only support
JavaScript 1.1, most notably Internet Explorer 3.0.
When the form is submitted, the onSubmit
handler
calls the validateForm
function. This function
builds an array of elements such as text boxes that require values,
an array of select list elements that require a selection, and an
array of radio button group elements that require a checked value.
These lists are passed to requireValues
,
requireSelects
, and
requireRadios
, respectively, which verify that
these elements have been filled in by the user.
Finally, the
checkProblems
function loops over the
properties in the validate object and returns a boolean value
indicating whether there are any elements that still have formatting
problems. If requireValues
,
requireSelects
,
requireRadios
, or
checkProblems
fail, then they display an
appropriate message to the user and return false, which cancels the
submission of the form. Otherwise, the form is submitted to the CGI
script which handles the query like any other request. In this case,
the CGI script would record the data in a file or database. We
won’t look at the CGI script here, although we will discuss
saving data like this
on the
server in Chapter 10.
Note that we said that the CGI script would handle a request coming from a page with JavaScript validation just like it would handle any other request. When you do data validation with JavaScript, there’s an important maxim you need to keep in mind: Never rely on the client to do your data validation for you. When you develop CGI scripts, you should always validate the data you receive, whether the data is coming from a form that performs JavaScript validation or not. Yes, this means that we are performing the same function twice. The theory behind this is that you should never trust data that comes from the client without checking it yourself. As we mentioned earlier, JavaScript may be supported by the user’s browser or it may be turned off. Thus, you cannot rely on JavaScript validation being performed. For a more detailed discussion of why it is a bad idea to trust the user, refer to Chapter 8.
Thus, we may often write our data validation code twice, once in JavaScript for the client, and again in our CGI script. Some may argue that it is poor design to write the same code twice, and they are right in that avoiding duplicate code is a good principle of designing maintainable code. However, in this situation, we can provide two counter-arguments.
First, we need to do data validation in the CGI script because it is also good programming practice for each component to validate its input. The JavaScript code is part of the client user interface; it receives data from the user and validates it in preparation for sending it to the server. It sends the data on to the CGI script, but the CGI script must again validate that the input it receives is in the proper format because the it doesn’t know (nor should it care) what processing the client did or did not do on its end. Similarly, if our CGI script then calls a database, the database will again validate the input that we sent on to it, etc.
Second, we gain much by doing JavaScript validation because it lets us validate as close to the user as possible. If we perform data validation on the client using JavaScript, we avoid unnecessary network connections because if JavaScript notices an invalid entry, it can immediately notify the user who can correct the form before it is submitted. Otherwise, the client must submit the form to the server, a CGI script must validate the input and return a page reporting the error and allowing the user to fix the problem. If there are multiple errors, it may take a few tries to get it right.
In many cases, performing the extra check with JavaScript is worth the trade-off. When deciding whether to use JavaScript validation yourself, consider how often you expect the interface and the format of the data to change and how much extra effort is involved in maintaining JavaScript validation code in addition to CGI script validation code. You can then weigh this effort against the convenience to the user.
If you place enough functionality in JavaScript-enabled web pages, they can become semiautonomous clients that the user can interact with independent of CGI scripts on the server. The most recent versions of JavaScript provide the ability to create queries to web servers, load the response in hidden frames, and react to this data. In response to queries such as these, CGI scripts are not outputting HTML; they’re typically outputting raw data that is being handled by another application. We’ll explore the concept of information servers further when we’ll discuss XML in Chapter 14.
As JavaScript’s abilities have expanded, one question that web developers sometimes ask is how they can move their complex data structures from their Perl CGI scripts into JavaScript. Perl and JavaScript are different languages with different data structures, so it can be challenging creating dynamic JavaScript.
Exchanging data between different languages isn’t a new challenge of course, and fortunately someone else has already addressed this same problem. Allaire, the makes of Cold Fusion, wanted a way to exchange data between different web servers on the Internet. Their solution, Web Distributed Data Exchange, or WDDX, defines a common data format that various languages can use to represent basic data types. WDDX uses XML, but you don’t need to know anything about XML to use WDDX because there are modules that provide a simple interface for using it in many languages including Perl and JavaScript. Thus, we can convert a Perl data structure into a WDDX packet that can then be converted into a native data structure in JavaScript, Java, COM (this includes Active Server Pages), ColdFusion, or PHP.
However, with JavaScript, we can even skip the intermediate step. Because converting data to JavaScript is such a common need on the Web, WDDX.pm, the Perl module for WDDX, will convert a Perl data structure into JavaScript code that can create a corresponding JavaScript data structure without creating a WDDX packet.
Let’s look at an example to see how this works. Say that you want to pass the current date on the web server from your CGI script to JavaScript. In Perl, the date is measured by the number of seconds past the epoch; it looks like this:
my $now = time;
To create JavaScript from this, you would use the following code:
use WDDX; my $wddx = new WDDX; my $now = time; my $wddx_now = $wddx->datetime( $now ); print $wddx_now->as_javascript( "serverTime" );
We create a WDDX.pm object and then pass the time to the
datetime
method, which returns a WDDX::Datetime
object. We can then use the as_javascript
method
to get JavaScript code for it. This outputs something like the
following (the date and time will of course be different when you run
it):
serverTime=new Date(100,0,5,14,20,39);
You can include this within an HTML document as JavaScript code. Dates are created very differently in JavaScript than in Perl but WDDX will handle this translation for you. DateTime is just one data type that WDDX supports. WDDX defines several basic data types that are common to several programming languages. The WDDX data types are summarized in Table 7.1.
Table 7-1. WDDX Data Types
WDDX Type |
WDDX.pm Data Object |
Perl Type |
---|---|---|
String |
WDDX::String |
Scalar |
Number |
WDDX::Number |
Scalar |
Boolean |
WDDX::Boolean |
Scalar (1 or " “) |
Datetime |
WDDX::Datetime |
Scalar (seconds since epoch) |
Null |
WDDX::Null |
Scalar (undef) |
Binary |
WDDX::Binary |
Scalar |
Array |
WDDX::Array |
Array |
Struct |
WDDX::Struct |
Hash |
Recordset |
WDDX::Recordset |
None (WDDX::Recordset) |
As you can see, the WDDX data types are different from Perl’s
data types. Perl represents many different data types as
scalars. As a result, the WDDX.pm module
works differently than similar WDDX libraries for other languages,
which are more transparent. In these other languages, you can use one
method to go directly from the native data type to a WDDX packet (or
JavaScript code). Because of the differences with the data types in
Perl, WDDX.pm requires that you create an intermediate data object,
such as $wddx_now
, the WDDX::Datetime object that
we saw above, which can then be converted to a WDDX packet or native
JavaScript code.
Although originally conceived by Allaire, WDDX has been released as an open source project. You can download the WDDX SDK from http://www.wddx.org/; the WDDX.pm module is available on CPAN.
WDDX.pm is most useful for complex data structures, so let’s look at another example. We’ll use JavaScript and HTML to create an interactive form that allows users to browse songs available for download (see Figure 7.3). Users can look through the song database without making additional calls to the web server until they have found a song they want to download.
We’ll maintain the song information in a tab-delimited file on the web server with the format shown in Example 7.3.
This record-based format is the same that is used by a spreadsheet or a database, and it is represented in WDDX as a recordset. A recordset is simply a series of records (or rows) that share a certain number of named fields (or columns).
Let’s look at the HTML and JavaScript for the file. Note that this version requires that the user have JavaScript; this form will not contain any information without it. In practice, you would probably want to add a more basic interface within <NOSCRIPT> tags to support non-JavaScript users.
A CGI script will output this file when it is requested, but the only thing our CGI script must add is the data for the music. Thus, in Example 7.4, we’ll use HTML::Template to pass one variable into our file; that tag appears near the bottom.
Example 7-4. music_browser.tmpl
<HTML> <HEAD> <TITLE>Online Music Browser</TITLE> <SCRIPT SRC="/js-lib/wddx.js"></SCRIPT> <SCRIPT> <!-- var archive_url = "http://www.some-mp3-site.org/downloads/"; function showArtists( ) { var artists = document.mbrowser.artistList; buildList( artists, "artist", "", "" ); if ( artists.options.length == 0 ) { listMsg( artists, "Sorry no artists available now" ); } showConcerts( ); showSongs( ); } function showConcerts( ) { var concerts = document.mbrowser.concertList; if ( document.mbrowser.artistList.selectedIndex < 0 ) { var selected = selectedValue( document.mbrowser.artistList ); buildList( concerts, "concert", "artist", selected ); } else { listMsg( concerts, "Please select an artist" ); } showSongs( ); } function showSongs( ) { var songs = document.mbrowser.songList; songs.options.length = 0; songs.selectedIndex = -1; if ( document.mbrowser.concertList.selectedIndex < 0 ) { var selected = selectedValue( document.mbrowser.concertList ); buildList( songs, "song", "concert", selected ); } else { listMsg( songs, "Please select a concert" ); } } function buildList( list, field, conditionField, conditionValue ) { list.options.length = 0; list.selectedIndex = -1; var showAll = ! conditionField; var list_idx = 0; var matched = new Object; // Used as hash to avoid duplicates for ( var i = 0; i < data[field].length; i++ ) { if ( ! matched[ data[field][i] ] && ( showAll || data[conditionField][i] == conditionValue ) ) { matched[ data[field][i] ] = 1; var opt = new Option( ); opt.text = data[field][i]; opt.value = data[field][i]; list.options[list_idx++] = opt; } } } function showSongInfo( ) { var form = document.mbrowser; var idx = -1; for ( var i = 0; i < data.artist.length; i++ ) { if ( data.artist[i] == selectedValue( form.artistList ) && data.concert[i] == selectedValue( form.concertList ) && data.song[i] == selectedValue( form.songList ) ) { idx = i; break; } } form.artist.value = idx > 0 ? data.artist[idx] : ""; form.concert.value = idx > 0 ? data.concert[idx] : ""; form.song.value = idx > 0 ? data.song[idx] : ""; form.venue.value = idx > 0 ? data.venue[idx] : ""; form.date.value = idx > 0 ? data.date[idx] : ""; form.duration.value = idx > 0 ? data.duration[idx] : ""; form.size.value = idx > 0 ? data.size[idx] : ""; form.filename.value = idx > 0 ? data.filename[idx] : ""; } function getSong( ) { var form = document.mbrowser; if ( form.filename.value == "" ) { alert( "Please select an artist, concert, and song to download." ); return; } open( archive_url + form.filename.value, "song" ); } function listMsg ( list, msg ) { list.options.length = 0; list.options[0] = new Option( ); list.options[0].text = msg; list.options[0].value = "--"; } function selectedValue( list ) { return list.options[list.selectedIndex].value; } // --> </SCRIPT> </HEAD> <BODY BGCOLOR="#FFFFFF" onLoad="showArtists( )"> <TABLE WIDTH="100%" BGCOLOR="#CCCCCC" BORDER="1"> <TR><TD ALIGN="center"> <H2>The Online Music Browser</H2> </TD></TR> </TABLE> <P>Listed below are the concerts available for download from this site. Please select an artist from the list at the left, a concert (or recording) by that artist from the list in the middle, and a song from the list on the right. All songs are available in MP3 format. Enjoy.</P> <HR NOSHADE> <FORM NAME="mbrowser" onSubmit="return false"> <TABLE WIDTH="100%" BORDER="1" BGCOLOR="#CCCCFF" CELLPADDING="8" CELLSPACING="8"> <INPUT TYPE="hidden" NAME="selectedRecord" VALUE="-1"> <TR VALIGN="top"> <TD> <B><BIG>1)</BIG> Select an Artist:</B><BR> <SELECT NAME="artistList" SIZE="6" onChange="showConcerts( )"> <OPTION>Sorry no artists available</OPTION> </SELECT> </TD> <TD> <B><BIG>2)</BIG> Select a Recording:</B><BR> <SELECT NAME="concertList" SIZE="6" onChange="showSongs( )"> <OPTION>Please select an artist</OPTION> </SELECT> </TD> <TD> <B><BIG>3)</BIG> Select a Song:</B><BR> <SELECT NAME="songList" SIZE="6" onChange="showSongInfo( )"> <OPTION>Please select a concert</OPTION> </SELECT> </TD> </TR><TR> <TD COLSPAN="3" ALIGN="center"> <H3>Song Information</H3> <TABLE BORDER="0"> <TR> <TD><B>Artist:</B></TD> <TD><INPUT NAME="artist" TYPE="text" SIZE="40" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Recording:</B></TD> <TD><INPUT NAME="concert" TYPE="text" SIZE="40" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Song:</B></TD> <TD><INPUT NAME="song" TYPE="text" SIZE="40" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Venue:</B></TD> <TD><INPUT NAME="venue" TYPE="text" SIZE="40" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Date:</B></TD> <TD><INPUT NAME="date" TYPE="text" SIZE="20" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Duration:</B></TD> <TD><INPUT NAME="duration" TYPE="text" SIZE="10" onFocus="this.blur( )"></TD> </TR><TR> <TD><B>Download Size:</B></TD> <TD><INPUT NAME="size" TYPE="text" SIZE="10" onFocus="this.blur( )"></TD> </TR> </TABLE> </TD> </TR><TR ALIGN="center"> <TD COLSPAN="3"> <INPUT TYPE="hidden" NAME="filename" VALUE=""> <INPUT TYPE="button" NAME="download" VALUE="Download Song" onClick="getSong( )"> </TD> </TR> </TABLE> </FORM> <SCRIPT> <!-- <TMPL_VAR NAME="data"> // --> </SCRIPT> </BODY> </HTML>
This document has a form, but it
doesn’t actually submit any queries directly: it has no
submit button and its
onSubmit
handler cancels any attempts to submit. The form is simply used as an
interface and includes lists for artist, concert, and song as well as
fields for displaying information on selected songs (refer back to
Figure 7.3).
In the first
<SCRIPT>
tag, this document loads the wddx.js file, which
is included in the WDDX SDK available at http://www.wddx.org/. This file contains the
JavaScript functions needed to
interpret WDDX objects like recordsets. When the file loads, all of
the JavaScript code outside of functions and handlers is executed.
That sets the archive_url
global to the URL of the
directory where the audio files are located; it also executes the
JavaScript code inserted by our CGI script for the <TMPL_VAR
NAME="song_data"> tag. We’ll come back to how this
JavaScript is generated when we look at the CGI script in a moment,
but let’s peek at the JavaScript code that will be inserted
here. It looks like this:[10]
data=new WddxRecordset( ); data.artist=new Array( ); data.artist[0]="The Grateful Dead"; data.artist[1]="The Grateful Dead"; data.artist[3]="Widespread Panic"; data.artist[4]="Widespread Panic"; data.artist[5]="Leftover Salmon"; data.artist[6]="The Radiators"; ...
The data
variable is an object with a property for
each field from our song_data.txt data file,
like artist
in this example. Each of these
properties is an array containing as many entities as there are rows
in the data file.
As soon as the browser renders the page, the
onLoad
handler calls the
showArtists
function. This function displays the
artists by calling buildList
for the artist
select list object. It then calls the
showConcerts
and showSongs
functions, which also use the buildList
function.
The buildList
function takes a select list
object, the name of the field to pull the data from, and two
additional parameters that are the name and value of a field to use
as a condition for displaying a record. For example, if you call
buildList
like this:
buildList( document.mbrowser.concertList, "concert", "artist", "Widespread Panic" );
then for every record where the artist is “Widespread
Panic”, the value of the concert
field is
added it to the concertList
select list. If the
conditional field name is not provided, then
buildList
adds the requested field for all
records.
Initially, the artist list is populated, the concert list has one entry telling the user to select an artist, and the song list has one entry telling the user to select a concert. Once the user selects an artist, the concerts by that artist appear in the concert list. When the user selects a concert, the songs from that concert appear in the songs list. When the user selects a song, the song information is displayed in the lower text fields.
These text fields all have the same handler:
onFocus="blur( )"
This handler essentially makes the text fields uneditable by the
user. As soon as the user tries to click or tab to one of the fields,
the cursor immediately leaves the field. This serves no purpose other
than to indicate that these fields are not intended for user input.
If the user is fast enough, it is actually possible to add text to
these fields, but it won’t affect anything. These fields are
populated by the showSongInfo
function. This
function looks through the data to determine which song has been
selected and then loads the information for this field into the text
fields and also sets the hidden filename
field.
When the user clicks on the Download Song button, its
onClick
handler calls the
getSong
function. getSong
verifies that a song has been selected by checking the value of the
filename field If no song has been selected, the user is notified.
Otherwise, the requested song is downloaded in another window.
Let’s look at the CGI script now. Our CGI script must read the data file, parse it into a WDDX::Recordset object, and add it as JavaScript to our template. The code appears in Example 7.5.
Example 7-5. music_browser.cgi
#!/usr/bin/perl -wT use strict; use WDDX; use HTML::Template; use constant DATA_FILE => "/usr/local/apache/data/music/song_data.txt"; use constant TEMPLATE => "/usr/local/apache/templates/music/music_browser.tmpl"; print "Content-type: text/html "; my $wddx = new WDDX; my $rec = build_recordset( $wddx, DATA_FILE ); # Create JavaScript code assigning recordset to variable named "data" my $js_rec = $rec->as_javascript( "data" ); # Output, replacing song_data template var with the JavaScript code my $tmpl = new HTML::Template( filename => TEMPLATE ); $tmpl->param( song_data => $js_rec ); print $tmpl->output; # Takes WDDX object and file path; returns WDDX::Recordset object sub build_recordset { my( $wddx, $file ) = @_; local *FILE; # Open file and read field names from first line open FILE, $file or die "Cannot open $file: $!"; my $headings = <FILE>; chomp $headings; my @field_names = split / /, lc $headings; # Make each field a string my @types = map "string", @field_names; my $rec = $wddx->recordset( @field_names, @types ); # Add each record to our recordset while (<FILE>) { chomp; my @fields = split / /; $rec->add_row( @fields ); } close FILE; return $rec; }
This CGI script starts like our previous examples: it adds the
modules we need, defines constants to the files it uses, and outputs
the HTTP header. Next, it creates a new WDDX object and constructs a
recordset
via the build_recordset
function.
The build_recordset
function takes a
WDDX object and a file path. It opens the file
and reads the first line into $headings
to
determine the names of the fields. It then splits these into an
array, making sure that each field name is lowercase. The next line
is a little more complex:
my @types = map "string", @field_names;
WDDX needs to know the data type for each field
in the recordset. In this instance, we can treat each field as a
string, so this script uses Perl’s map
function to create an array the same size as
@field_names
with every element set to
"string"
and assign it to
@types
. It then gets a new WDDX::Recordset object
and loops through the file, adding each line to the recordset.
We then convert the recordset into JavaScript code and parse this
into the template, replacing the song_data
tag.
That JavaScript code we
discussed
earlier takes
over from
WDDXthere.
We’ll end this chapter with a much less common use of JavaScript: bookmarklets. Bookmarklets are JavaScript URLs that have been saved as bookmarks. The basic concept behind bookmarklets has been around since JavaScript was first created, but it has been slowly growing in popularity since Steve Kangas first coined the term bookmarklet and created a web site devoted to them at http://www.bookmarklets.com/. Many people consider bookmarklets a novelty, but they have a much greater potential. Bookmarklets really shine when they are combined with custom CGI scripts, which is why they are of interest to us.
First, let’s see how bookmarklets work. Bookmarklets are much easier to show than to explain, so let’s look at the world’s most popular program, “Hello World,” as a bookmarklet. The source for it is as follows:
javascript:alert("Hello world!")
If you were to type this into your browser as a location, it would display the alert shown in Figure 7.4.
You can enter this directly into your browser because this simple program is also a valid URL. The javascript scheme tells browsers, which support it, that they should interpret the rest of the URL as JavaScript code in the context of the current web page and return the result as a new web page. You can also create hyperlinks that have this format. If you were to embed the following into an HTML web page, then you could click on the link to get the alert as well:
<A HREF='javascript:alert("Hello world!")'>Run Script</A>
However, neither of these examples are actually bookmarklets until you save the URL as a bookmark in your browser. Doing so is browser-specific, of course. Most browsers allow you to click on a hyperlink with your right mouse button and choose an option to save the link as a bookmark. Once you have done this, you have captured the script as a bookmarklet that you can run whenever you want by choosing it from your list of bookmarks.
Let’s look at a more complicated example. We have referenced RFCs several times thus far. Let’s make a bookmarklet that allows you to look up a particular RFC. In this case, we’ll use http://www.faqs.org/rfc/ as the RFC repository.
Here is how we might write the JavaScript for this:
rfcNum = prompt( "RFC Number: ", "" ); if ( rfcNum == parseInt( rfcNum ) ) open( "http://www.faqs.org/rfc/" + rfcNum + ".html" ); else if ( rfcNum ) alert( "Invalid number." );
We ask the user for an RFC number. If the user enters an integer, we open a new browser window to fetch the corresponding RFC. Note that we don’t handle the case in which the RFC doesn’t exist; the user will simply get a 404 error from the http://www.faqs.org web server. However, if the user enters a value that isn’t a number, we do report that error to them. If the user enters nothing or clicks Cancel, we do nothing.
Now let’s convert to this to a bookmarklet. First, we must need
to make sure we do not return any values from our code. If the code
in your bookmarklet returns a value, some browsers (including
Netscape’s) will replace the current page with the value. You
will confuse users if, for example, they get an empty page with a
[null]
in the top left corner every time they use
your bookmarklet. The easiest way to avoid returning a value is to
use the void
function. It takes any value as an
argument and returns nothing. We can wrap the
void
function around the last statement that
returns a value, or simply append it to the end. We’ll do the
latter because in this script there are three different lines that
could be executed last, depending on the user’s entry. So we
add the following line to the end of our script:
void( 0 );
Next, we should need to remove or encode any
characters that are not valid within a URL. This includes whitespace
and the following characters: <
,
>
, #
, %
,
"
, {
, }
,
|
, ,
^
,
[
, ]
,
`
.[11] However,
Netscape
Communicator 4.x will not recognize encoded syntax elements (such as
brackets) within JavaScript URLs. So although it means that
bookmarklets containing these characters are invalid URLs, if you
want your bookmarklets to work with Netscape’s browsers, you
must leave these characters unencoded. Other browsers accepts these
characters encoded or unencoded. In any event, you should remove any
unnecessary whitespace.
Finally, we prefix our code with javascript:
, and
we get the following:
javascript:rfcNum=prompt('RFC%20Number:',''),if(rfcNum==parseInt(rfcNum)) open('http://www.faqs.org/rfc/'+rfcNum+'.html'),else if(rfcNum) alert('Invalid%20number.'),void(0);
The line endings are not part of the URL but have been added to allow it to fit on the page.
There is one more thing that you should keep in mind when working
with bookmarklets. Bookmarklets execute in the same scope as the
frontmost page displayed in the user’s browser. This has a
number of advantages as we will see in the next section, Section 7.4.2. The disadvantage is that you must be
careful that the code you create does not conflict with other code
that is on the current page. You should be especially careful with
variable names and create names that are very unlikely to appear on
other web sites.
Variables are case-sensitive in
JavaScript; using odd combinations of capitalization in variables is
a good idea. In our last example, rFcNuM
may have
been a better (though less readable) choice as a variable name.
Because bookmarklets use JavaScript, they are not compatible with all web browsers. Some browsers that support JavaScript, such as Microsoft Internet Explorer 3.0 do not support bookmarklets. Other browsers impose limitations on bookmarklets. Unless you’re distributing your bookmarklets as unsupported novelties, you should do extensive testing. Bookmarklets use JavaScript in a less than traditional manner, so test them with as many different versions of as many different browsers on as many different platforms as you can.
You should also keep your bookmarklets short. Some browsers do not impose a limit on the length of a URL; others limit URLs to 255 characters. This can even vary by platform: for example, Communicator 4.x allows only 255 characters on MacOS while it allows much longer URLs on Win32.
One of the features that some users of bookmarklets promote is that bookmarklets avoid some of JavaScript’s browser incompatibility issues. Because Netscape and Microsoft have different implementations of JavaScript, if you want to create a bookmarklet that uses incompatible features of each, you can create two different bookmarklets instead of one bookmarklet that attempts to support both browsers. Then people can choose the bookmarklet that is appropriate to their browser. The problem with this approach is that Netscape and Microsoft are not the sole distributors of web browsers. Although these two companies create the majority of browsers on the web, there are other high-quality browsers that also support JavaScript and bookmarklets, such as Opera, and these browsers are growing in popularity. If you start supporting specific browsers, you may find yourself needing to choose which browsers to support and which users you are willing to loose. Hopefully, ECMAScript and DOM will quickly provide standards across all browsers.
So what do bookmarklets provide us as CGI developers? Bookmarklets can do anything that JavaScript can do including displaying dialog boxes, creating new browser windows, and generating new HTTP requests. Furthermore, because they execute in the context of the browser’s frontmost window, they can interact with objects or information in this window without the security restrictions that an HTML window from your site would encounter. Thus, bookmarklets provide a very different or even transparent interface to our CGI scripts.
Let’s look at an example. Say that you want to be able to create and store comments for web pages as you surf that you can retrieve when you visit the web pages later. We can do this with a simple bookmarklet and CGI script. First, let’s create the CGI script.
Our CGI script needs to do two things. It needs to accept a URL and a comment and record them. It also needs to be able to retrieve a comment when given a particular URL. Example 7.6 provides the code.
Example 7-6. comments.cgi
#!/usr/bin/perl -wT use strict; use CGI; use DB_File; use Fcntl qw( :DEFAULT :flock ); my $DBM_FILE = "/usr/local/apache/data/bookmarklets/comments.dbm"; my $q = new CGI; my $url = $q->param( "url" ); my $comment; if ( defined $q->param( "save" ) ) { $comment = $q->param( "comment" ) || ""; save_comment( $url, $comment ); } else { $comment = get_comment( $url ); } print $q->header( "text/html" ), $q->start_html( -title => $url, -bgcolor => "white" ), $q->start_form( { action => "/cgi/bookmarklets/comments.cgi" } ), $q->hidden( "url" ), $q->textarea( -name => "comment", -cols => 20, -rows => 8, -value => $comment ), $q->div( { -align => "right" }, $q->submit( -name => "save", -value => "Save Comment" ) ), $q->end_form, $q->end_html; sub get_comment { my( $url ) = @_; my %dbm; local *DB; my $db = tie %dbm, "DB_File", $DBM_FILE, O_RDONLY | O_CREAT or die "Unable to read from $DBM_FILE: $!"; my $fd = $db->fd; open DB, "+<&=$fd" or die "Cannot dup DB_File file descriptor: $! "; flock DB, LOCK_SH; my $comment = $dbm{$url}; undef $db; untie %dbm; close DB; return $comment; } sub save_comment { my( $url, $comment ) = @_; my %dbm; local *DB; my $db = tie %dbm, "DB_File", $DBM_FILE, O_RDWR | O_CREAT or die "Unable to write to $DBM_FILE: $!"; my $fd = $db->fd; open DB, "+<&=$fd" or die "Cannot dup DB_File file descriptor: $! "; flock DB, LOCK_EX; $dbm{$url} = $comment; undef $db; untie %dbm; close DB; }
We use a disk-based hash called a DBM file in order to store comments and URLs. The tie function associates a Perl hash with the file; then anytime we read from or write to the hash, Perl automatically performs the corresponding action on the associated file. We will cover how to use DBM files in more detail in Chapter 10.
The JavaScript that we will use to call this CGI script is as follows:
url = document.location.href; open( "http://localhost/cgi/bookmarklets/comments.cgi?url=" + escape( url ), url, "width=300,height=300,toolbar=no,menubar=no" ); void( 0 );
As a bookmarklet, it looks like this:
javascript:dOc_uRl=document.location.href;open('http://localhost/cgi/bookmarklets comments.cgi?url='+escape(dOc_uRl),dOc_uRl,'width=300,height=300,toolbar=no, menubar=no'),void( 0 )
If you save this bookmarklet, visit a web site, and select the bookmarklet from your bookmarks, your browser should display another window. Enter a comment and save it. Then browse other pages and do the same if you wish. If you return to the first page and select the bookmarklet again, you should see your original comment for that page, as in Figure 7.5. Note that the comments window will not update itself each time you travel to another page. You will need to select the bookmarklet each time you want to read or save comment for a page you are on.
If you were to distribute this bookmarklet to friends, the comments would be shared and you could see what each other has to say about various web sites. The CGI script could also be placed in a secure directory and be extended to maintain separate databases for each user; you may want users to only be able to read other users’ comments.
We would not have been able to build an application like this with a standard HTML page due to JavaScript’s security restrictions. One HTML page cannot access objects in another HTML page if the two pages are from different domains (i.e., different web servers), so our comment form cannot determine the URL of any other browser windows. However, bookmarklets circumvent this restriction. Browsers allow this because the user must actively choose to run a bookmarklet in order for it to execute.
There are numerous other ways that you can put bookmarklets to use. You can see many examples of bookmarklets that use existing Internet resources at http://www.bookmarklets.com. Many of these are novelties, but bookmarklets can do more. Bookmarklets are most powerful when you have goods or services that can take advantage of accessing information on other sites as people surf. For example, companies such as the Better Business Bureau could offer bookmarklets that users can select when they are on another site to see how that site has been rated. Companies that sell add-on products or services like warranties can provide users with a bookmarklet that users can select when they are going to make a purchase online. Other possibilities are up to you to create.
[9] Some web servers do support server-side JavaScript, but not via CGI.
[10] Incidentally, all of the artists listed here have released statements affirming that their policy has been to allow their fans to record and distribute their performances for noncommercial purposes, and new digital music formats, such as MP3, do not alter this position. In other words, it is legal to distribute MP3s of their live performances (and a handful of other recordings released electronically). Obviously, it would be illegal to create a site like this with copyrighted music.
[11] Control and non-ASCII characters are invalid as well, but these values must be escaped within JavaScript anyhow. Also, you may notice that this list is different than the list provided in Section 2.1.3. That list is for HTTP URLs, so it includes characters that have special significance to HTTP. JavaScript URLs are different than HTTP URLs, so this list includes only characters considered illegal for all URLs.