Chapter 7. JavaScript

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.).

Background

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.

History

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.

Compatibility

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.

Forms

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.

Input Validation

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.

Validating elements

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.

JavaScript alert

Figure 7-1. JavaScript alert

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.

Validating submits

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.

Validation example

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).

Our sample user registration form

Figure 7-2. Our sample user registration form

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.

Validating twice

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.

Data Exchange

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.

WDDX

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.

Example

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.

Example 7-3. song_data.txt

Artist  Concert  Song  Venue  Date  Duration  Size  Filename
...

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.

Online music browser

Figure 7-3. Online music browser

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.

Bookmarklets

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.

Bookmarklet Basics

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.

Result from our “Hello World” bookmarklet

Figure 7-4. Result from our “Hello World” bookmarklet

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.

Compatibility

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.

Bookmarklets and CGI

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.

Updating a comment to comment.cgi via a bookmarklet

Figure 7-5. Updating a comment to comment.cgi via a bookmarklet

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset