Professional Documents
Culture Documents
By Steve Schafer
What Is WAP?
WAP stands for Wireless Access Protocol, a general term used to
describe the multi-layered protocol and related technologies that bring
Internet content to mobile devices such as PDAs and cell phones.
Such devices are referred to as thin clients because they have one or
more constraints in the form of display, input, memory, CPU, or other
hardware or usability limitations. The platform constraints and the
slower (and more expensive) bandwidth of cellular and related
networks make standard Internet protocols difficult to utilize. Using
the growing set of WAP tools and protocols, however, the mobile
Internet is quite a capable tool.
What Is WML?
WML (Wireless Markup Language) is the dominant language in use
with wireless devices today. Essentially, WML is a subset of HTML, but
has its roots in XML. Those developers with a solid base in XML should
have a relatively easy time coding WML.
The current WML standard is 1.3, although many mobile devices in use
today support only the WML 1.1 standard. Therefore it's prudent to
stay away from 1.3-specific features, unless you know that your target
market's devices are 1.3-ready.
There are several key differences between WML and standard HTML,
including the following:
WML is highly structured and very particular about syntax. Several
current HTML browsers allow for "messy" code such as missing tags
and other formatting snafus. Such mistakes are not allowed in WML;
the mobile browser will complain and generally won't display the
page.
WML is case sensitive. The tags <b> and <B> are treated as
different tags, although they accomplish the same purpose (bold
text). Therefore, you must be careful to match the case of your
opening tags with your closing tags (for example, <b>This is
bold</B> will not work as expected).
Many tags have required attributes. Developers accustomed to HTML
may be used to including only attributes they need-in some WML
tags, you must include a few attributes, even if they are blank or
default.
WML pages are structured in "decks" (see the next section), allowing
for multiple pages to be defined in each WML file.
WML also has a client-side scripting language, WMLScript, to help
automate particular tasks, validate input, and so on. WMLScript is a
subset of JavaScript and will be covered in a later article.
Understanding Decks
WML pages are structured within "decks," allowing several pages
("cards") to be defined in each WML file. This deck analogy allows
multiple pages to be delivered to the mobile client at the same time,
minimizing the loading time between related pages. However, the
limited memory on most devices constrains the deck size, usually to
less than 1024 bytes. Therefore, careful consideration and planning
should go into any WAP application; don't start coding without
investing time in planning.
Note: Remember your audience. Mobile users generally scroll through
cards rapidly and will be reading on a display that's a mere handful of
characters wide (usually less than 20 characters) and usually less than
10 lines high. Keep your content to a minimum, provide an intuitive
navigation structure, and optimize your decks to maximize links within
the deck and minimize links outside of the deck.
Visualizing a physical "deck of cards" structure can help in
understanding the principles of WML. For example, suppose we have
three simple cards (pages) as shown below:
Figure 1.3 - As the user follows the links through the deck, the
cards stack up in memory.
A developer accustomed to HTML might be tempted to implement the
"back" feature by providing a link to the deck, specifying the previous
card. However, this would cause the mobile device to re-request the
Figure 1.4 - The <prev> tag "pops" the top card off the stack
(out of the history list), redisplaying the previous card in the
history.
To add MIME types to your Web server, you must have administrator
access to the server. The following sections cover setting up
Microsoft's Internet Information Server (IIS) and Apache for WML. If
you're using another type of server, read your server's documentation
for more information on adding MIME types.
Adding MIME Types to Internet Information Server (IIS)
To add the MIME types to IIS, open the Internet Information Services
Management Console (MC). Access to this console varies depending on
which specific operating system you're using and how you installed
IIS, but can usually be found under Administrative Tools (Windows
2000) or Option Pack (Windows NT).
Open the IIS MC, click on the server to expand its tree, and then rightclick on Default Web Site and choose Properties. (Note: If you don't
want all the sites on your server to be able to deliver WML, right-click
on those sites you want to be WML-enabled and then continue
following these steps.)
Click on the HTTP Headers tab and then click on the File Types button
under the MIME Map section. In the File Types dialog, click on New
Type and enter the extension and MIME definition ("Content Type")
from the preceding table. Click on OK. Repeat this process for the
other two MIME types.
When you're finished, close the Web Site Properties dialog by clicking
on OK. On some servers, there may be nodes or devices that also
define HTTP codes and need to inherit the new setting(s). Choose the
appropriate options for your system. Exit the IIS MC. Usually you
won't need to restart the IIS service, but it wouldn't hurt to do so just
in case.
Tip: Before exiting the Web Site Properties, you may want to add an
entry for WML on the Documents tab (such as index.wml). This causes
the server to display that document by default, eliminating the need
for your visitors to specify a particular file in the URL to access your
site.
Adding MIME Types to Apache
To add MIME types to Apache, you must edit the httpd.conf file. This
file's location varies from system to system.
This file uses "AddType" lines to define MIME types. Find the section
where these appear and add the following lines:
AddType text/vnd.wap.wml .wml
AddType text/vnd.wap.wmlscript .wmls
AddType image/vnd.wap.wbmp .wbmp
Save and close the file and restart the Apache server to reload the
configuration with the new MIME types.
Tip: You may want to add "index.wml" or comparable entry to the
DirectoryIndex section of the Apache configuration file (requires
running mod_dir). This causes the server to display that document by
default, eliminating the need for your visitors to specify a particular file
in the URL to access your site.
The first line of the preceding code specifies that the file is XML and
version 1.0-compatible. The second line defines the XML scope of the
file; namely, that its DOCTYPE is WML, and where the Document Type
Definition (DTD) can be found.
Tip: If you're unfamiliar with XML and don't fully understand these
lines, just make sure that they appear at the beginning of all your
WML documents.
The next line begins the WML definition with the <wml> tag.
The <card> tag defines a card in the deck. Note that the id and title
attributes can be anything you choose, but should be short and to the
point, and the id must contain only letters and numbers (no
punctuation or spaces).
If you want to try the preceding example, create the file on your Web
server (in plain text form), adding the following lines between the
<card> tags to provide content for the card:
<p>
A Sample Card
</p>
SDK WAP Edition are trademarks of Openwave Systems Inc. All rights
reserved.)
Documentation
There is an abundance of WAP/WML documentation on the Web,
mostly courtesy of the Open Mobile Alliance (OMA) and other WAP
organizations. You should spend some time reading the user interface
(UI) suggestions, developer guidelines, and language reference text at
the sites listed in the following sections.
Tip: No emulator can fully replace actual device testing. Although you
can initially code and debug on one of the emulators, don_t neglect
testing on your actual target device(s) as well.
The Gelon Wapalizer - http://www.gelon.net
The Wapalizer is a Web-based WAP emulator and offers several skins
to emulate popular phones. Simply type your URL into the Wapalizer
box and click the Wapalize button to open your code in the default
emulator. Alternatively, choose a different skin and use the emulated
device's interface to navigate to your URL.
Note: The Wapalizer is not as robust as some of the other emulators.
Although it's a handy way to quickly check a page, you should also use
an emulator that closely resembles your target microbrowser.
The Openwave SDK - http://developer.openwave.com
The Openwave SDK 5.1 provides the best suite of tools I've run
across; I use it as my primary coding and testing environment. The
Openwave developer site (http://developer.openwave.com) has
several SDKs available, depending on what WML language revision and
feature set you need. SDK 5.1 can be used for most applications,
providing that you stay within the coding boundaries for your target
device's supported version of WML and you fully test your code on the
target device(s).
Nokia Emulators - http://www.forum.nokia.com
Nokia maintains an impressive list of emulators, including several that
tie in to their Mobile Internet Toolkit. Note that you must register on
the site to download any of the tools. Many are Java-based, and
therefore require you to install a Java Virtual Machine (JVM) on your
system to run the tool.
Other Emulators
If your device isn't compatible with the emulators listed above, visit
the manufacturer's site for the device to see whether they offer a
compatible tool.
Tip: If you need to find out what browser your target device uses,
access a nonexistent file on your Web server and examine your server
logs for the browser identification information. (A nonexistent file
makes it easy to find the pertinent log entry.) For example, if I use my
Samsung A400 phone to access "missingfile.jpg" on my Web server, I
find the following log entry:
[25/Jun/2002:16:27:50 -0500] "GET /wireless/missingfile.jpg
HTTP/1.1" 404 306 "-" "SEC-spha400 UP.Browser/4.1.22b1
UP.Link/5.0.2.3c"
This entry shows the client ("spha400") and the browser and version
(UP.Browser 4.1.22b, now the Openwave Mobile Browser, but still
identified as UP.Browser).
You may remember the document preamble code (first two lines) from
the previous article. These lines are necessary to identify the type of
content (WML) to the browser. They need to be present in all files.
The deck is then started with a <wml> tag, and ended with a </wml>
tag.
Note that the <head> section is optional, but is necessary to supply
any meta information (via <meta> tags) required for your deck.
Typically, cache instructions are passed via meta tags included in the
<head> section. Leaving the <head> section blank by including only
the <head> tags is a bad idea; it can cause some browsers to fail.
Include this section only if you need to include <meta> or <access>
tags.
What follows is the actual deck, made up of individual cards, each with
a mandatory ID and title parameter. (Other parameters for the
<card> and other tags will be covered in later articles.) Some
browsers display the card title at the top of the display; others don't.
It's important to make your card titles display-ready (short but
meaningful, and so on).
Tip: It may be helpful to have this information stored in a file as a
template for all your WML coding. Note that some SDKs provide a
skeletal WML structure when you create a new file.
WML Language Rules
Let's quickly review some basic WML language rules:
Most tags have opening and closing components. Those that don_t
(<access>, <br/>, <img>, <meta>, and so on) include a slash at
the end of the tag, signifying that the tag is singular (opening and
closing).
The language is case sensitive, so all closing tags must match the
capitalization of the opening tags. (<B>This is bold</b> will not
have the desired results.)
All tag parameter values must be enclosed in quotes (for example,
<p mode="nowrap">).
All text must be enclosed in a tag, even if that tag is just a simple
paragraph tag (<p>).
There are some elements that must appear in a certain order. For
Use
Mark blocks of text as paragraphs.
Break the current line, much like pressing Enter in a word processing
Boldface the delineated text.
Make the text appear in a large point size.
Emphasize the text (usually with italics).
Display the text in an italic font.
Display the text in a small point size.
Display the text in a strongly emphasized font. (Usually a large point
Display the text in an underlined font.
Note: Many of these text attributes (such as <big>, and so on) are not
supported by all mobile browsers.
The most important text formatting tag is the paragraph tag (<p>).
This tag must appear around all text not delineated by other tags. It
also controls how the browser presents the text if you add the optional
mode attribute. The two supported modes are "wrap" and "nowrap",
with "wrap" as the default. The wrap mode allows the text to flow
down the device screen, breaking lines at appropriate spaces and
punctuation. The nowrap mode uses a marquee-like display to display
the text, scrolling the text horizontally when this mode is selected.
For example, take the following code:
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card id="card1" title="Text Wrapping">
<p mode="nowrap">
No Wrap: The quick brown fox jumped over the lazy dog.
</p>
<p mode="wrap">
Wrap: The quick brown fox jumped over the lazy dog.
</p>
</card>
</wml>
Figure 2-2 - Text set to nowrap will scroll horizontally when the
browser selects that line. Note how the first line (nowrap) has
scrolled in this figure. The text will continue to scroll until it
reaches the end of the paragraph, where it will start over.
Image courtesy Openwave Systems Inc. (Openwave, the Openwave
logo, Openwave SDK, Openwave SDK Universal Edition, Openwave
SDK WAP Edition are trademarks of Openwave Systems Inc. All rights
reserved.)
If you don't use the mode attribute, the client browser uses the
currently selected mode, or the default "wrap" if no mode had been
defined previously. Typically the nowrap mode is used for headlines
and selection text, while wrap is used for general and body text.
ard Navigation
Now let's consider how to navigate between cards within a deck. As an
example, we'll use a riddle, having the user navigate between the
question and answer.
Take the following riddle:
Q: You cannot see it, you cannot touch it, it isn't a liquid, it isn't a
Card 1
The first card is simply text, enclosed in paragraph tags. However,
we've added a <do> element to change the behavior of the Accept key
- one of the default navigation keys on most mobile devices (see the
next section on keys for more information). This <do> element has
type and label attributes. The type attribute tells the browser how the
<do> is called. In this case it's by remapping the Accept key's
function. The label supplies a label for the key, namely "Answer" in
this example.
Note: This label bends one of the UI suggestions of using a maximum
of five letters for key labels.
Within the <do> element is a <go> instruction, supplying the address
(href) to "go" to when the key is pressed. The href is in the following
form:
<Deck>#<CardID>
Since the destination is in the same deck, the deck name can be
omitted and only the CardID is necessary.
Note: The order of the <do> and <p> elements is arbitrary. I prefer
to put my navigation directives before my text wherever possible. It
would be acceptable to reverse their order. Whichever you choose it
pays to be consistent.
Card 2
The second card also uses a paragraph element, but incorporates a
link within the element by using the <anchor> tag. The form of the
<anchor> tag is as follows:
<anchor title="label">
type
text
</anchor>
Question".
Note that the <anchor> is enclosed in the paragraph and that the
paragraph is set to nowrap. This will prevent the link from wrapping to
several lines. We aren't worried about the plain text ("Silence")
wrapping in this example, but we could easily enclose the <anchor> in
its own nowrap <p> tag and change the text paragraph to wrap.
Result
The above code has the following result:
Figure 2-3 - The left display is Card1, the right is Card2. Notice
that the emulator displays a check mark instead of the
standard "Link" text for the Accept button.
Image courtesy Openwave Systems Inc. (Openwave, the Openwave
logo, Openwave SDK, Openwave SDK Universal Edition, Openwave
SDK WAP Edition are trademarks of Openwave Systems Inc. All rights
reserved.)
Expanding the Example
You might have noticed that our second navigation method - mapping
Then, duplicate cards 1 and 2 and re-label them as 3 and 4. You also
need to change the link in card 3 (to card 4) and supply a new
question and answer. The result would be similar to the following:
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card id="card1" title="Question">
<do type="accept" label="Answer">
<go href="#card2" />
</do>
<p mode="wrap">
You cannot see it, you cannot touch
it, it isn't a liquid, it isn't a
solid, it isn't a gas, but it can
be broken. <br/>
What is it?
</p>
</card>
<card id="card2" title="Answer">
<p mode="nowrap">
Silence.<br/>
<br/><br/>
<anchor>
<go href="#card3" />
Next Question
</anchor>
</p>
</card>
<card id="card3" title="Question">
The Accept and Option keys are referred to as soft keys because you
can redefine their meaning and default labels with the software, in this
case WML code. Many of the navigation elements allow you to specify
which keys you want the soft keys to interact with. For example, in our
riddle example we specified "accept" to modify the Accept key. We
could have used "options" to redefine the other soft key (on the lower
right side of the display).
Tip: When defining multiple <do> elements on the same card, the
affected soft key will be labeled "MENU" and allow access to a menu of
defined <do> elements.
Unfortunately, there are few firm standards for default keys. Some
device manufacturers map different keys to the WAP interface, while
some have more proprietary solutions such as a jog wheel to move
between options and functions. The best thing to do before planning
an interface is to read the UI and application style guides and test your
target devices.
</card>
<card id="green" title="Green">
<p>
Your choice was Green.
</p>
</card>
<card id="blue" title="Blue">
<p>
Your choice was Blue.
</p>
</card>
</wml>
Tip: Notice the addition of the "title" parameter in each <option> tag.
The text provided with this parameter is displayed as the Accept soft
key's label when the option is selected.
We used the options to navigate to cards within the current deck.
However, you could just as easily navigate to other decks or specific
cards within those decks.
Type
Onpick
Onenterforward
Onenterbackward
Ontimer
Event
User selects or deselects an <option> item.
User navigates to a card in a forward manner
(e.g., from a <go> or other link)
User navigates to a card in a backward manner
(e.g., from a <prev> element or Back command)
A specified <timer> element expires
In the following example, the deck displays a splash screen before the
main menu:
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card id="splash" title="Welcome!">
<onevent type="ontimer">
<go href="#main" />
</onevent>
<timer name="delay" value="50"/>
<p mode="wrap">
Welcome to ACME Incorporated.
</p>
</card>
In this form, the element accepts user input and stores it in the
variable specified. The input is freeform; that is, it is not constrained
to any particular format (numeric, alphanumeric, etc.) or length-although most mobile browsers impose a length limit of 256
characters.
Note: A full discussion of WML variables is outside the scope of this
article, but will be provided in the next article. For this exercise it is
important to know that variables exist and can be set by <input> and
<setvar> elements, among others. A variable can then be referenced
by prefixing its name with a dollar sign ("$"), or using the preferred
method of enclosing the name in parentheses and prefixing the whole
structure with a dollar sign. For example, the variable "firstname" can
be referenced as "$firstname" or "$(firstname)". When the variable is
referenced, WML will substitute the variable's value for its reference.
The <input> element also supports the optional parameters shown in
the following table.
Parameter Use
Title
Supplies a title for the element. Some devices display this
title in the default Accept soft key label; others display it
as "tooltip" text while the element is selected.
Type
Set to "text" or "password," this parameter controls
whether the text is displayed as it is entered or displayed
in the browser's password character(s) (usually "*").
Value
The default value for the element. Note that if the
variable specified in the "name" parameter already has a
value, or if the value specified with this parameter doesn't
conform to the specified "format" parameter, the value is
ignored.
Accesskey
Format
Emptyok
Maxlength
When using the same character multiple times in a row, you can prefix
Notice that the <input> element uses the "M" format mask, supplying
an uppercase letter as the default first character. The "maxlength"
parameter constrains the input's length, despite the "*" in the mask.
The deck navigation in this example is controlled by a <do> element
linked to the Accept key. In some devices the user will have to press
the Accept key twice--once to exit the input element and another to
Note that the "alt" and "src" parameters are mandatory in all <img>
tags. The <img> tag also supports the following optional parameters:
Parameter Use
localsrc
Specifies an icon. (See the later section on icons.)
align
Aligns the image relative to the current line of text.
Supports "top," "middle," and "bottom."
height
Specifies the exact height of the displayed image.
width
Specifies the exact width of the displayed image.
hspace
Specifies how much white space should be displayed at
each side of the image (horizontal space).
vspace
Specifies how much white space should be displayed
above and below the image (vertical space).
Note: Not all mobile browsers support the sizing and spacing
parameters.
Incorporating the logo into our previous splash screen example, we get
the following result (see figure):
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
Note that the "alt" and "src" tags are mandatory, even if left blank.
Tip: When using icons for ornamentation, be sure to specify alternate
text that can serve the same purpose or that resembles the icon.
Let's suppose that ACME is a travel agency. We could add an airplane
icon to the bottom of the splash screen with this tag (see figure):
<img alt="airplane" src="" localsrc="168" />
Understanding Variables
WML supports variables that can hold transitional data between cards,
provide custom output tailored to individual users, and more.
Variables are special holding places for values. They can hold numeric
or alpha values and their values can be changed by code at the
programmer's whim. However, it is usually good practice to dedicate
variables to particular purposes, and hence, to particular types of data.
Note: WML reserves the ampersand symbol (&) for entities. To use this
symbol in your deck, you must substitute the corresponding entity
"&".
Setting Variables
Variables can be set by the following elements:
<input>
<select>/<option>
<setvar>
Note: The <input> element accepts a "value" attribute that can set a
default value for the input. However, if the variable specified has
already been assigned a value, the "value" attribute is ignored.
The <select> and <option> Elements
Each <option> element within a <select> can also set variables. When
the user selects an option, that option's "value" parameter is stored in
the variable specified by the select's name parameter. Although it
sounds convoluted, it is pretty straightforward in practice. The
following example would store the selected color's hex value in the
variable "color":
<select name="color">
<option value="#FF0000">Red</option>
<option value="#00FF00">Green</option>
<option value="#0000FF">Blue</option>
</select>
For example, if the user selected "Green," the variable "color" would
be set to "#00FF00".
Note: The <select> element accepts a "value" attribute that can set a
default value for the input. However, if the variable specified has
already been assigned a value, the "value" attribute is ignored.
The <setvar> Element
The <setvar> element is perhaps the most pure of the variable-setting
elements, because it serves the sole purpose of simply setting a
variable. In the following example, the variable "color" is set to
"Green":
<setvar name="color" value="Green" />
appropriate variables when the user enters this card from a forward
direction:
<card id="card1">
<onevent type="onenterforward">
<refresh>
<setvar . . . />
<setvar . . . />
</refresh>
</onevent>
Note: Unlike the other elements above, the <setvar> element sets the
value of the specified variable whether or not it has previously been
assigned a value.
Using Variables
Variables are very flexible and can be used in a variety of ways. You
can store the results of user choices or status flags, display variable
text, etc. Using the <postfield> element, you can even pass a
variable's value to an external program.
The <postfield> element defines a name/value pair to be sent to the
HTTP server with the next <go>. The <postfield> element is
encapsulated within <go> elements as follows:
<go . . . >
<postfield name="name" value="value" />
...
</go>
Note that the "value" attribute is mandatory and is not ignored if the
variable has been set previously. To send a value of a variable that has
been set elsewhere, use the variable tag as the "value" attribute. For
example, the following code will send the current value of the color
variable:
<postfield name="color" value="$(color)" />
Note: You can also include variables in the URL of <go> and other
navigation elements by using the standard "?name=value" notation:
<go href="colorselect.cgi?color=$(color) />
23. </wml>
sample.wmls
24. extern function multiply() {
25. var Num = WMLBrowser.getVar("num");
26. var NumX2;
27.
28.
29.
30.
NumX2 = Num * 2;
WMLBrowser.setVar("numX2", NumX2);
WMLBrowser.go("sample.wml#card2");
}
Notice that the WMLScript file begins with an "extern" keyword in front
of the function definition. This keyword allows a function to be
accessed from outside the file; for this example, from our WML deck.
Functions accessed internally by the same WMLScript file do not need
this keyword.
Our example executes as follows:
1 As the first card is displayed, the <onevent> handler causes the
variable "num" to be initialized as blank (lines 5-9). Although not
completely necessary, this step is good practice to ensure that the
variable exists and is in a known state.
2 Input is received via the <input> element and stored in the variable
"num" (line 14). Note that the "format" parameter forces numeric
input.
3 The related <do> element (lines 11-13) calls the "multiply" function
in the wmls file.
4 The "multiply" function declares two variables, setting the variable
"Num" to the value of the XML variable "num," by using the
WMLBrowser.getVar function (lines 25-26).
5 The variable "Num" is multiplied by 2; the resulting value is stored in
the variable "NumX2" (line 27).
6 The script stores the value of NumX2 into a WML variable
("numX2"), using the WMLBrowser.setVar function (line 28).
7 The script uses a "go" function to jump to the second card in the
WML deck (line 29).
8 The second card (lines 17-22) displays our text, using the variables
"num" and "numX2."
Note: The WMLBrowser library functions (WMLBrowser.setVar, etc.)
provide an easy interface between XML and XMLScript. Consult a
WMLScript language reference for more information on these
functions.
ensure that we have two uppercase letters, it can't match the input
against an acceptable list. For that, we will use WMLScript.
input.wml
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card id="state" title="Enter State">
<onevent type="onenterforward">
<refresh>
<setvar name="stateabbr" value=""/>
</refresh>
</onevent>
<p>
<do type="accept" label="Validate">
<go href="validate.wmls#state()"/>
</do>
<input name="stateabbr" format="AA" />
</p>
</card>
<card id="stateok" title="StateValid">
<p>
$(stateabbr) is a valid State code.
</p>
</card>
</wml>
validate.wmls
extern function state() {
var stateabbr = WMLBrowser.getVar("stateabbr");
var pos, x;
var okay = false;
var statelist =
"ALAKASAZARCACOCTDEDCFMFLGAGUHIIDILINIAKS
KYLAMEMHMDMAMIMNMSMOMTNENVNHNJNMNYNCNDMP
OHOKORPWPAPRRISCSDTNTXUTVTVIVAWAWVWIWY";
if (String.length(stateabbr) == 2) {
pos = String.find(statelist,stateabbr);
x = pos / 2;
if (Float.int(x) == x) { okay = true; }
}
if (okay) {
WMLBrowser.go("input.wml#stateok");
} else {
Dialogs.alert("Invalid input!");
WMLBrowser.go("input.wml#state");
}
}
The concept is relatively simple. The WML file takes the input, stores it
in the "stateabbr" variable, and then calls the validation function. The
function grabs the state abbreviation that was entered and performs
two checks:
Is the abbreviation two characters long?
Does it appear in a valid list?
The String library function "length" is used to determine whether the
abbreviation is two characters long. If so, the second check is
performed. The second check attempts to find the abbreviation in the
"statelist" string. The position is then divided by two-if the result is an
integer, the state is valid.
Note: A simple substring search ("String.find") is not enough to
validate the input, as combinations like "SK" would pass the test. The
abbreviation must be in an even-numbered position (evenly divisible
by 2). If the abbreviation is not found, the find function returns -1,
which also fails the even number test.
WMLScripting
In the last article we discussed how to use WMLScript to perform basic
scripting tasks within your WML pages. As mentioned in that article,
WMLScript can be used to validate input and perform basic operations
on WML variables. WMLScript can also be used to control the display of
various cards and the information contained on them. For example,
the following code will display an animation by changing an image's
source and refreshing a card every two seconds:
animation.wml
<wml>
<card id="card1">
<onevent type="onenterforward">
<refresh>
<setvar name="num" value="1" />
<setvar name="image" value="image1.wbmp" />
</refresh>
</onevent>
<onevent type="ontimer">
<go href="junk.wmls#main()" />
</onevent>
<timer value="20" />
<p><img alt="$(image)" src="$(image)" /></p>
</card>
<card id="card2">
<p>Animation Done!</p>
</card>
</wml>
animation.wmls
extern function main() {
var Num = WMLBrowser.getVar("num");
var Image = "";
if (Num < 9) {
Num++;
Image = "image" + Num + ".wbmp";
WMLBrowser.setVar("num", Num);
WMLBrowser.setVar("image", Image);
WMLBrowser.refresh();
} else {
WMLBrowser.go("animation.wml#card2");
}
}
Note the addition of ".wml" to the definition. This will cause the server
to parse all .wml files with PHP. Don't forget to restart Apache so it will
read the new configuration. Make sure that your WML definitions
appear before the PHP definition to ensure that Apache recognizes
".wml" as a valid type.
Tip: As mentioned above, this causes all .wml files to be parsed by
PHP. Although this is usually desirable, you could create another mime
type ("pwml") and include it in both the WML and PHP definitions.
Then you could continue to use .wml for pure WML files and the new
type for PHP-parsed WML.
Cross-breeding with WML: IIS
To enable WML pages to be parsed by PHP, you need to add PHP as an
application to handle your .wml files.
To do this, follow the steps below:
1 Open the Internet Information Services Management Console.
2 Right-click on the site where you want to add this functionality (or
use Default Web Site) and choose Properties.
3 Access the Home Directory tab and click the Configuration button
near the bottom of the dialog.
4 Click the Add button to add an application.
5 Add the path and filename of the PHP executable (usually
"C:\PHP\php.exe") in the Executable field and ".wml" in the
Extension field.
6 Ensure that "Script Engine" and "Check that file exists" are both
checked.
7 Select OK back to the IIS MC.
8 Although not usually required, you may need to restart your Web
server.
In step 6, above, setting the "Check that file exists" option is not
entirely necessary, but can be helpful in debugging server issues.
Without that option being set, IIS will pass all requests for .wml pages
to PHP, even if the requested page doesn't exist. Instead of generating
a "404 - Not Found" error, the Web server will report a problem with
PHP (which failed because there was no file to parse).
These steps will result in all .wml files being parsed by PHP. Although
this is usually desirable, you could create another mime type ("pwml")
and include it in the WML definition and define PHP as an application
for the new type. Then you could continue to use .wml for pure WML
files and the new type for PHP-parsed WML.
Getting the Client to Recognize PHP-parsed WML
Now that the server has been configured to deliver PHP-parsed WML,
you need to ensure that the client will receive it as WML. This is
accomplished by having each PHP-parsed page send the appropriate
header to the client.
You should begin each PHP file with the following line:
header("Content-type: text/vnd.wap.wml");
This line needs to come before anything else is sent to the client-it
pays to make a habit of including it right after the "<?php" start tag.
The following example shows how to display a one-card deck with the
text "Hello world" using PHP:
<?php
header("Content-type: text/vnd.wap.wml");
print "<?xml version=\"1.0\"?>\n";
print "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\""
. " \"http://www.wapforum.org/DTD/wml_1.1.xml\">\n";
print
print
print
print
print
"<wml>\n";
"<card>\n";
"<p>Hello world</p>\n";
"</card>\n";
"</wml>\n";
?>
Note that there are several ways to accomplish sending the WML to
the client. I've chosen to use individual print statements, each ending
with a newline code. This keeps the resulting WML fairly readable and
PHP mimics the line breaks. In some cases, namely long stretches of
code, a print "here document" structure would work better. The above
example would resemble the following in "here document" structure:
print <<<END
<?xml version=\"1.0\"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card>
<p>Hello world</p>
</card>
</wml>
END;
In any case, remember that your target platform is WML-stick to WMLcompatible tags and structure and avoid using reserved characters
(such as "$") in places where they might be misinterpreted. Also,
debugging the code is twice as hard as straight WML because you have
to debug the PHP code as well as the resulting WML. The next article in
this series will provide more debugging tips.
Utilizing the Power of PHP
As previously mentioned, PHP's interoperability with other technologies
makes it a powerful ally for WML. One such technology, databases, is
especially useful.
As an introduction, we'll cover a basic example here. The next article
in the series will showcase more examples of using PHP to extend
WML.
For this example we'll use MySQL, a popular open-source database
solution, although any database with PHP connectivity would work as
well.
Suppose that we have a team of salespeople in the field who need
regular access to customer data. In this example we'll only be
concerned with the customer's phone number, but it illustrates the
underlying power of using PHP while creating a useful "online"
phonebook.
Our database for this example is very
First Name
Last Name
John
Smith
Kathy
Lamarr
Sam
Kinkaid
Holly
Haute
simple:
Phone Number
555-723-0900
555-876-2222
555-243-8766
555-327-0987
In MySQL, the database and table would be created using the following
code:
CREATE DATABASE Customer;
USE Customer;
INTO
INTO
INTO
INTO
Phone
Phone
Phone
Phone
VALUES
VALUES
VALUES
VALUES
('John','Smith','555-723-0900');
('Kathy','Lamarr','555-876-2222');
('Sam','Kinkaid','555-243-8766');
('Holly','Haute','555-327-0987');
SDK WAP Edition are trademarks of Openwave Systems Inc. All rights
reserved.)
Quite a few changes can be made to the code above:
The "order by" clause of the query can be changed to sort by a
different field or by multiple fields.
Although the current layout allows each record to be displayed in its
entirety, using the "wrap" option in the <p> tags would allow more
records to be displayed per screen (but each would have to be
highlighted for the full record to be seen).
This example is convenient because there are only four rows in our
database. To make our user interface more "friendly," we should
create paging code to step through the data nine rows at a time
(ensuring that each row has a quick access key [1-9] associated with
it).
that tells the script card that it is supposed to display. The script
recursively calls itself with the proper value of $cmd, matching the
function it is supposed to perform next. The script's functionality
resembles the following:
If $cmd is empty, set it to "Menu"
If $cmd is set to "Menu," display the Menu card
If $cmd is set to "List," display the List card
If $cmd is set to "Display,"
display the Display card, with the appropriate record
If $cmd is set to "Search," display the Search card
When a function is chosen,
set $cmd appropriately and recursively call self.
The search function will search through the database looking for first
or last names that meet the text entered in the search card. The list
function will display five records at a time and will include options for
moving to the next set of five records or returning to the home menu.
Each option (record or link) will have a quick key. Essentially, the list
will resemble the following:
1 <DB record>
2 <DB record>
3 <DB record>
4 <DB record>
5 <DB record>
6 [Next 5 link]
7 [Home link]
Selecting a record results in displaying all of the data associated with
the record, along with "dial" and "home" links.
LastName
Phone
Address
Hampton
317-555-2200
46222
2002-08-22
Samuel
Marks
317-555-8764
46203
2002-08-22
Sally
Nash
317-555-8765
46875
2002-08-22
City
State Zip
LastUpdate
213 Main St
2 Northridge Dr
644 Innovation Pl
Indianapolis
IN
Fishers
IN
Ft Wayne
IN
Bill
Haskins
317-555-8766
201 W 103rd
Indianapolis
46240
2002-08-24
Jill
Payton
317-555-0098
55 W 96th St
Westfield
IN
46222
2002-08-22
Mary
Martinez
317-555-7544
9433 E. 75th Ave
Greenwood
IN
46784
2002-08-22
Ned
Tanner
317-555-9877
77 E Marchen
Ft Wayne
IN
46875
2002-08-23
Bruce
Wilten
317-555-1111
3 Prospect
Indianapolis
IN
46038
2002-08-22
Naomi
Waters
317-555-4323
1121 Central Pl
Westfield
IN
46055
2002-08-22
Angela
Renault
317-555-0988
5674 E 6th Ave
Noblesville
IN
46234
2002-08-22
Markus
Elliot
317-555-3232
9755 Carter
Indianapolis
IN
46250
2002-08-22
Steve
Albert
317-555-5444
95 Crescent Dr
Indianapolis
IN
46250
2002-08-26
Martin
Rolfsen
317-555-6767
5678 E 7th Ave
Indianapolis
IN
46234
2002-08-22
Lisa
Biggins
317-555-3644
7732 Allisonville
Indianapolis
IN
46240
2002-08-22
Eric
Gonday
317-555-0500
9466 Pike Plaza
Greenfield
IN
46533
2002-08-22
Douglas
Poser
317-555-0123
55 Tower Pl
Noblesville
IN
46234
2002-08-24
John
Palmer
317-555-4444
12433 N Cumberland
Fishers
IN
46038
2002-08-22
IN
The database will also contain an "ID" field as a primary key (integer).
That will serve as a unique key into each record. To create the data in
MySQL, the following commands should be used:
CREATE DATABASE customers;
USE customers;
CREATE TABLE Phone (
Id int(11) NOT NULL auto_increment,
FirstName varchar(30) default NULL,
LastName varchar(30) default NULL,
Phone varchar(12) default NULL,
Address varchar(30) default NULL,
City varchar(30) default NULL,
State char(2) default NULL,
Zip varchar(5) default NULL,
LastUpdate date default NULL,
PRIMARY KEY (Id)
) TYPE=MyISAM;
INSERT INTO Phone VALUES (1,'Jack','Hampton','317-555-2200',
'213 Main St','Indianapolis','IN','46222','2002-08-22');
INSERT INTO Phone VALUES (2,'Samuel','Marks','317-555-8764',
'2 Northridge Dr','Fishers','IN','46203','2002-08-22');
The Cards
The following sections list the PHP code used for each function/card.
Note that the WML and header code is taken care of by global
statements-each card need only output the <card> tags and
everything in-between. The $cmd variable controls what function is
executed, and hence, which card is displayed.
Note: I've used "echo" commands in this script, but "print" commands
would do just as well. Also, to increase the readability of the output
I've added line feeds (via a variable, $lf, set to ASCII character 10) to
most lines of output. I've chosen to append this variable to the "echo"
statements for readability purposes--using "\n" in your "echo"
That ensures that if no command is given (via $cmd), the menu will be
displayed.
List
This is the meat of the script, displaying both the raw list as well as
search results, five records at a time. (For reference after the listing,
each line has been numbered.)
1 // Construct appropriate *count* query
2: $query = "select count(*) from Phone";
3: if (!empty($search)) {
4:
$query = $query." where FirstName like \"%".$search."%\" or";
5:
$query = $query." LastName like \"%".$search."%\"";
6: }
7:
8:
$result = mysql_query($query,$link)
or die("Query failed:$query");
9:
list($total_rows) = mysql_fetch_array($result);
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
$result = mysql_query($query,$link)
or die("Query failed:$query");
20:
21:
// Advance DB index
$next = $idx + 5;
22:
23:
24:
// Start card
echo "<card id=\"Contacts\">\n";
echo "<do type=\"accept\" label=\"View\"> <go href=\"\"/> </do>".$lf;
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
$recordid = $line[Id];
$Name = $line[LastName] . ", " . $line[FirstName];
$Number = $line[Phone];
$Prompt = $Name . " (" . $Number . ")";
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
// Close tags
echo "\">[End of List]".$lf."</option>".$lf;
64:
65:
66:
67:
68:
69:
70:
// Close select
echo "</select>".$lf;
71:
72:
// Close card
echo "</p>".$lf."</card>".$lf;
This code makes use of global variables ($idx, $search) to display the
list of contacts. Both variables are passed as a name/value pair when
the script is called with $cmd equal to "List." If $search is empty, the
full list of contacts is displayed, else the text of $search is added to the
query and records are returned only if FirstName or LastName contains
the search text.
The $idx variable marks what results the script currently is listing. The
record at location $idx is the first record on the current page.
This becomes more self-explanatory as we work through the code:
Lines 1-9 construct and execute a "count" query, storing the number
of returned rows in the variable $total_rows. This value is used later
(line 48) to determine whether there are more pages of data to
display. Note that the search text is added if $search is not empty
(hence contains search criteria).
Lines 10-19 construct a query to return the target dataset. Again, the
search criterion is added to the query, if it exists. Line 17 appends a
limit clause to the query, causing the query to return only five records
(or less), starting at the record indicated by the value of $idx. If $idx
is zero (which it will be the first time the script executes List), the first
five records are returned. The variable $next is set to a value of $idx +
5 (line 21) and used to call the next iteration of List, causing the next
five records to be displayed.
Note: This method is far from perfect. For example, if a record is
added or modified that causes a record to be added or removed from
the returned dataset, the next page will return different results than it
would before the record was added or modified. If the list is being
displayed while the records are being modified-which happens in most
database applications-the displayed results can be somewhat
unpredictable.
Lines 22-30 begin the display card definition, including the appropriate
header-"Phone Book" if the search string is empty (raw list being
displayed) or "Search Results" if the search string is not empty (search
results being displayed). This helps guarantee that the result set
returned will be the same, allowing consistent paging through the set
(with the caveat explained above).
Line 32 begins the <select> list, with lines 34-70 building and
displaying five items as <option>s. Lines 35-38 build the text for the
<option> prompt, while lines 39-44 construct the <option> statement
with an appropriate "onpick" parameter that recursively calls the script
with $cmd equal to "Display" and the ID of the record to display. Note
that $idx and $search are also passed to maintain their values through
the recursive call, just in case we need them later.
Lines 49-64 build option number 6 in our select list. If there are more
records to display ($total_rows >= $next) the script generates the
option "[Next Records]" with an appropriate "onpick" parameter to
recursively call the script, specifying the next starting record to display
(via idx=$next). If there are no more records to display, the script
generates an "[End of List]" option, which recursively calls the script
with the same starting point as is currently displayed. Each option also
includes the search criteria if it exists (lines 51-54 and 58-61).
Finally, a "Home" option is created (lines 65-68) to allow the user to
return to the home menu from any page of the listing. The open tags
are then closed.
Search
The search function is simply an input tag that accepts up to 10
characters and recursively calls the script supplying the text entered
and sets $cmd equal to "List."
echo "<card id=\"Search\">\n";
echo "<do type=\"accept\" label=\"Go\">".$lf;
"<p>".$lf;
"<b>Phone Book Search</b><br/>Search for:".$lf;
"<input name=\"searchtext\" title=\"Search\" type=\"text\"";
" format=\"10m\"/>";
"</p>".$lf;
"</card>".$lf;
Display
Display uses the record ID passed in $idx to select a record from the
database and display all of its related information (name, address,
phone, etc.).
echo "<card>\n";
// Get specific record
$query = "select * from Phone where Id = \"".$idx."\"";
$result = mysql_query($query,$link)
or die("Query failed:$query");
// Get data and display
while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
$recordid = $line[Idx];
echo
echo
echo
echo
echo
echo
echo
echo
"<p mode=\"wrap\">".$lf;
"$line[LastName], $line[FirstName]<br />".$lf;
"$line[Address]<br />".$lf;
"$line[City], $line[State] $line[Zip]<br />".$lf;
"<a href=\"wtai://wp/mc;$line[Phone]\" title=\"Dial\">";
"$line[Phone]</a><br />".$lf;
"<a href=\"?cmd=Menu\" title=\"Menu\">";
"[Home Menu]</a><br /><br />".$lf;
Note that the card has a "Dial" option mapped to the Accept key and is
displayed with the phone number highlighted. This allows the user to
quickly dial the selected number on devices that support URL dialing.
Other items of note include a "Home Menu" link and a more verbose
format for the last update date. We do not need to provide any
functionality to return to the last page of record listings-the user can
do so by pressing the Back key on his or her device.
if (empty($idx)) { $idx = 0; }
if (empty($cmd)) { $cmd = "Menu"; }
switch ($cmd) {
case "Menu";
echo "<card id=\"Menu\">\n";
echo "<p mode=\"nowrap\">".$lf;
// Set up Select menu list
echo "<select name=\"Select\" title=\"Select:\">".$lf;
// Go through results from Query, listing each as a CHOICE entry
echo "<option onpick=\"?cmd=List\">";
echo "List Contacts</option>".$lf;
echo "<option onpick=\"?cmd=Search\">";
echo "Search Contacts</option>".$lf;
// Close select
echo "</select>".$lf;
// Close card
echo "</p>".$lf."</card>".$lf;
break;
case "List";
// Construct appropriate *count* query
$query = "select count(*) from Phone";
if (!empty($search)) {
$query = $query." where FirstName like \"%".$search."%\" or";
$query = $query." LastName like \"%".$search."%\"";
}
$result = mysql_query($query,$link)
or die("Query failed:$query");
list($total_rows) = mysql_fetch_array($result);
// Construct appropriate query
$query = "select * from Phone";
if (!empty($search)) {
$query = $query." where FirstName like \"%".$search."%\" or";
$query = $query." LastName like \"%".$search."%\"";
}
case "Search";
echo
echo
echo
echo
echo
"<card id=\"Search\">\n";
"<do type=\"accept\" label=\"Go\">".$lf;
"<go href=\"?cmd=List&search=\$searchtext\">".$lf;
"</go>".$lf;
"</do>".$lf;
echo
echo
echo
echo
echo
"<p>".$lf;
"<b>Phone Book Search</b><br/>Search for:".$lf;
"<input name=\"searchtext\" title=\"Search\" type=\"text\"";
" format=\"10m\"/>";
"</p>".$lf;
echo "</card>".$lf;
break;
case "Display";
echo "<card>\n";
// Get specific record
$query = "select * from Phone where Id = \"".$idx."\"";
$result = mysql_query($query,$link)
or die("Query failed:$query");
// Get data and display
while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
$recordid = $line[Idx];
echo
echo
echo
echo
echo
echo
echo
echo
"<p mode=\"wrap\">".$lf;
"$line[LastName], $line[FirstName]<br />".$lf;
"$line[Address]<br />".$lf;
"$line[City], $line[State] $line[Zip]<br />".$lf;
"<a href=\"wtai://wp/mc;$line[Phone]\" title=\"Dial\">";
"$line[Phone]</a><br />".$lf;
"<a href=\"?cmd=Menu\" title=\"Menu\">";
"[Home Menu]</a><br /><br />".$lf;
Figure 3 - The end of the list. Notice the [End of List] option.
Figure 5 - The results of a search (for "in"). Note the first result
is "Biggins, Lisa" but the text has scrolled to the phone number
due to the "nowrap."
Conversion Procedures
Converting standard HTML-formatted text is a two-step process. First,
remove any tags that are not supported by the target platform.
Second, tailor supported tags to the target platform. For example, the
line break tag is supported by WML, but needs to have the slash added
("<br />").
Removing Unsupported Tags
To ensure a smooth conversion, remove all but the following tags from
the HTML code:
<p>
</p>
<br>
You can also retain text-formatting tags that your target browser
supports, such as <i>, <b>, etc.
If you are using PHP, the code to strip the offending tags is very
simple:
$wml = strip_tags($html,'<p><br><i><b><u>');
Using the HTML (stored in $html), the above code removes all tags but
those given in the "strip_tags" parameter, and stores the result in the
variable $wml.
If you are feeling adventurous and know the format of tables in the
code, you can parse the table tags down to the bare minimum
parameters (as supported by your target browser). However, only the
smallest tables will display conveniently on mobile devices.
Converting Supported Tags
Although paragraph (<p>) and line break (<br />) tags are supported
in WML, their usage varies from that in HTML. For example, blocks of
text must be enclosed in paragraph tags; you cannot use a stray tag
to separate paragraphs, like this:
Paragraph . . .
<p>
Paragraph . . .
Although such use is sloppy when used anywhere, it has become
prevalent in HTML pages. Instead of creating a sophisticated parsing
scheme to ensure the matching pairs of tags, it's much easier to
convert all open and closing paragraph tags to double line break tags.
This causes the current line to break where the paragraph tag was
used, and inserts the extra space between the paragraphs.
Again, if you are using PHP, the code is straightforward:
$wml = str_replace("<p>","<br /><br />",$wml);
$wml = str_replace("</p>","<br /><br />",$wml);
The above code will replace every "<p>" and "</p>" with "<br /><br
/>".
Each line break tag in WML must end in a slash. A similar PHP
str_replace statement takes care of this requirement:
$wml = str_replace("<br>","<br />",$wml);
Project Specifications
Hangman is a straightforward game. For anyone unfamiliar with the
game, a word is shown as blanks and the player guesses letters that
may be in the word. If a letter guessed is in the word, all instances of
that letter are revealed. If a guessed letter is not in the word, part of a
hanging stick figure is drawn on a scaffold, at the end of a
noose_usually starting with the head, then the body, then the limbs
(one at a time). The game ends when the word is guessed or the
figure is completely drawn. In the former case the player wins, in the
latter the player loses.
The functionality for our project is as simple as the pen-and-paper
version of the game:
1 A word is chosen and displayed as blanks.
2 The player picks a letter.
3 The letter is compared to the letters in the word; if the letter is in
the word, the appropriate blanks are changed to display the letter.
If the letter is not in the word, a piece is added to the hanging stick
figure.
4 The game ends when the user knows the word or the figure is
complete.
Once the functionality has been determined, the interface must be
drafted.
All output must fit on a display that is approximately 16 characters
wide and 4-10 lines tall.
The stick figure will be represented by ASCII characters instead of
graphics since we can't ensure that the player will be using a
15
16
17
18
19
20
21
22
</card>
<card id="card2" title="Hangman">
<p>
$word<br/>
$blank
</p>
</card>
</wml>
WMLS
1 extern function initword() {
2 // Define word list (pseudo array)
3 var words = "animal announce banana doctor elephant giraffe";
4 var idx,x = 0;
5 var blank,word = "";
6 // Randomize a word
7 idx = Lang.random(6);
8 word = String.elementAt(words,idx," ");
9 // Add an "*" for every letter in the chosen word
10
for (x = 1; x <= String.length(word); x++ ) {
11
blank = blank + "*";
12
}
13
// Set the WML vars and call card2
14
WMLBrowser.setVar("word", word);
15
WMLBrowser.setVar("blank",blank);
16
WMLBrowser.go("hangman.wml#card2");
17
}
The first design decision is how to store the word list. Because
standard WML technologies do not provide a convenient link to the
outside world (databases, etc.), the list has to be stored internally,
within the script itself. The lack of true arrays in WMLScript contributes
to the problem as well. However, WMLScript's string handling solves
the problem well enough_the words will be stored in delimited form
within one long string (WMLS line 3). The String.elementAt function
could then be used to parse one of the words from the list (WMLS line
8).
Next the "blank" template must be constructed for the word (lines 912). At this point, the variable $word will contain the actual word,
while $blank contains the blank representation. The variables are
pushed out to the browser and the next card is displayed, showing all
the values.
Note: Even this part of the script was developed in pieces, though for
the sake of brevity in this article I've chosen to start with this chunk of
code. Initially, I created the stub WML code to call the init function,
which simply set a variable. Then I created the word list and
randomized a word, which was displayed by the WML. Finally, I
created the blanking code to create the blank word.
Tip: The Openwave SDK provides a great environment to develop
applications in a stairstep method. Visit the developer site at
www.openwave.com for more information or to download the SDK.
Next we need to add the player entry code and the ability to check the
entry against the word. The new code resembles the following listings:
WML
1 <?xml version="1.0"?>
2 <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
3 "http://www.wapforum.org/DTD/wml_1.1.xml">
4 <wml>
5 <card id="card1" title="Hangman">
6 <onevent type="onenterforward">
7 <go href="hangman.wmls#initword()"/>
8 </onevent>
9 <p>
10
Initializing...
11
</p>
12
</card>
13
<card id="card2" title="Hangman">
14
<do type="accept">
15
<go href="hangman.wmls#guess()" />
16
</do>
17
<p>
18
Guess: <input name="guess" maxlength="1" format="a" />
19
$word<br/>
20
$blank
21
</p>
22
</card>
23
</wml>
WMLS
1 extern function initword() {
2 var words = "animal announce banana doctor elephant giraffe";
3 var idx,x,hang = 0;
4 var blank,word = "";
5 idx = Lang.random(6);
6 word = String.elementAt(words,idx," ");
7 for (x = 1; x <= String.length(word); x++ ) {
8 blank = blank + "*";
9 }
10
WMLBrowser.setVar("word", word);
11
WMLBrowser.setVar("blank",blank);
12
WMLBrowser.setVar("hang",hang);
13
WMLBrowser.go("hangman.wml#card2");
14
}
15
extern function guess() {
16
var x = 0;
17
var temp,ch = "";
18
var word = WMLBrowser.getVar("word");
19
var blank = WMLBrowser.getVar("blank");
20
var guess = WMLBrowser.getVar("guess");
21
// Check each letter in word. Transfer letters/blanks
22
// from blank, revealing any new matches.
23
for (x = 0; x <= String.length(word); x++ ) {
24
ch = String.subString(word,x,1);
25
if ( ch == guess ) {
26
temp = temp + guess;
27
} else {
28
temp = temp + String.subString(blank,x,1);
29
}
30
}
31
WMLBrowser.setVar("blank",temp);
32
WMLBrowser.go("hangman.wml#card2");
33
}
The new code (mostly the guess function, WMLS lines 15-33) accepts
user input (WML line 18) and compares it to the letters in the word. A
"temporary" word is built, revealing each matching letter and leaving
the rest of the word as revealed or hidden as it was before the guess.
The temporary variable ($temp) is then passed back to the browser as
0
-|/\
He is crude but recognizable. We can break the man down into six
pieces: the head, torso, two arms, and two legs. If we get creative and
treat each piece as a string, we can add appropriate line breaks and
print each string in sequence, effectively "building" the man. Consider
the following:
Head: "<space>0<linebreak>"
Arm1: "-"
Torso: "|"
Arm2: "-<linebreak>"
Leg1: "/"
Leg2: "<space>\"
When printed in sequence, the hanging man is displayed. If we print
only the first three strings, only three pieces of the man (head, arm,
torso) are displayed.
Again, because WMLS lacks real arrays, we will build a delimited string
that contains all the pieces:
" , 0\r,-,|,-\r,/,\\\r"
Note that a comma is used as the delimiting character, and that we
put a blank piece (a space) in the beginning to give the first real piece
an index of 1 instead of 0. Also note that we use escape codes for line
breaks (\r = newline) and we must escape the backslash. Because
WML doesn't parse variables, using WML code for line breaks would
only result in the tags (such as "<br />") being displayed as text
instead of being interpreted as line breaks.
Finishing Up
Although it seems like there is a lot left to do, the project is mopped
up pretty quickly by doing the following:
Adding a variable and code to count the incorrect guesses and
determine loss
Checking whether the word has been totally revealed (win)
Adding appropriate cards for win/loss
Building a string variable to display the hanging man
Various housekeeping and cleanup chores (renaming some
functions, cleaning up some variables, adding comments, etc.)
Most of these tasks revolve around counting the number of guesses.
The last task is the usual housekeeping that takes place at the end of
a project. The final code looks like this:
WML
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<!-- hangman.wml - WML hangman game -->
<wml>
<!-- Game initialization - only calls init script -->
<card id="init" title="Hangman">
<onevent type="onenterforward">
<go href="hangman.wmls#init()"/>
</onevent>
<p>
Initializing...
</p>
</card>
<!-- Status card. Shows hanged man and revealed word -->
<!-- User presses Accept key to make a guess -->
<card id="status" title="Hangman">
<do type="accept" label="Guess">
<go href="#guess" />
</do>
<p>
|<br/>
$man<br/><br/>
$blank
</p>
</card>
<!-- Accepts user's guess (one character, lowercase) -->
<card id="guess" title="Hangman">
<do type="accept">
<go href="hangman.wmls#guess()" />
</do>
<p>
$blank<br/>
Guess: <input name="guess" maxlength="1" format="a" />
</p>
</card>
<!-- Game over, player has been hanged. -->
<card id="hung" title="Hangman">
<do type="accept" label="Restart">
<go href="#init" />
</do>
<p>
You've been hanged!<br/><br/>
Word was:<br/>
$word<br/>
Your guess:<br/>
$blank
</p>
</card>
<!-- Game over, player wins..-->
<card id="win" title="Hangman">
<do type="accept" label="Restart">
<go href="#init" />
</do>
<p>
You win!<br/><br/>
Word was:<br/>
$word<br/>
</p>
</card>
</wml>
WMLS
// hangman.wmls
// Functions for hangman game
//
//
//
//
//
//
//
Variables:
word = word to be guessed
blank = blank representation of 'word', letters
begin as asterisks ("*") and are revealed as guessed
hang = current count of incorrect guesses
man = current ASCII representation of hanging man
guess = current character guessed
// Initialize game
extern function init() {
// Init vars. Change words every so often (or add to them)
var words = "animal announce banana doctor elephant giraffe";
var idx,x,hang = 0;
var blank,word,man = "";
// Pick a random word from list
idx = Lang.random(6);
word = String.elementAt(words,idx," ");
// Build a blank string (letters all "*") that
// is the same length as our word
for (x = 1; x <= String.length(word); x++ ) {
blank = blank + "*";
}
// Pass all values to browser
WMLBrowser.setVar("word",word);
WMLBrowser.setVar("blank",blank);
WMLBrowser.setVar("hang",hang);
WMLBrowser.setVar("man",man);
// Display status card
WMLBrowser.go("hangman.wml#status");
}
// Evaluate current guess
extern function guess() {
// Init vars
var x = 0;
var temp = "";
var correct = false;
var man = "";
// Pieces for the hanging man,comma delimited
var manpieces = " , 0\r,-,|,-\r,/, \\\r";
} else {
WMLBrowser.go("hangman.wml#status");
}
}
}
Things to Add
There's always room for improvement, especially in programming and
interfaces. Given time, I'd add the following to the game:
A graphic version of the hanging man as a default, allowing users
to switch to text if necessary.
Tracking and displaying the incorrect characters guessed
A splash screen (introductory graphical screen), help text, and a
cheat function (displays one character not yet revealed)
I'd also lengthen the word list and dynamically change it from time to
time. One idea would be to run a script on the server that swaps
different versions of the WMLS file into place, each with a different
word list. Another idea is to implement the entire game using PHP or
another, more robust scripting language that could tie into a massive
word database.
#!/usr/bin/perl
use Time::ZoneInfo ':all';
my $zones = Time::ZoneInfo->new();
foreach my $zone ($zones->zones) {
print $zone."\n";
}
This script simply creates a list of all the available zones and outputs
each to the console. Next, we need to know what time it is in each
zone. Fortunately, the operating system (Linux, in this case) can do
the work for us.
Most Linux applications that need to tell time do so by referencing the
environment variable TZ. This variable contains the current time zone
for the system. The OS can use this variable to decode its internal time
clock into the correct value for the zone. If you change this variable
and request the time from an application that uses it, you can easily
tell the time in another zone.
With help from the Date::Calc module, we can add the current time
zone to our test:
Listing: tztest.pl
#!/usr/bin/perl
...
America/New_York: 2002-12-29 23:35:22
America/Detroit: 2002-12-29 23:35:22
America/Louisville: 2002-12-29 23:35:22
America/Kentucky/Monticello: 2002-12-29 23:35:22
America/Indianapolis: 2002-12-29 23:35:22
America/Indiana/Marengo: 2002-12-29 23:35:22
America/Indiana/Knox: 2002-12-29 23:35:22
America/Indiana/Vevay: 2002-12-29 23:35:22
America/Chicago: 2002-12-29 22:35:22
America/Menominee: 2002-12-29 22:35:22
America/North_Dakota/Center: 2002-12-29 22:35:22
America/Denver: 2002-12-29 21:35:22
America/Boise: 2002-12-29 21:35:22
America/Shiprock: 2002-12-29 21:35:22
America/Phoenix: 2002-12-29 21:35:22
America/Los_Angeles: 2002-12-29 20:35:22
America/Anchorage: 2002-12-29 19:35:22
America/Juneau: 2002-12-29 19:35:22
America/Yakutat: 2002-12-29 19:35:22
America/Nome: 2002-12-29 19:35:22
America/Adak: 2002-12-29 18:35:22
...
Note that we simply change the TZ environment variable and call the
System_Clock method. The variable is only changed within the
application space, so the change in time zone doesn't affect the whole
system--just our application.
Searching for Time Zones
Next, we need the ability to search for a particular time zone. Adding a
variable, "findzone", and a simple substring search accomplishes this
goal:
Listing: tztest.pl
#!/usr/bin/perl
use Time::ZoneInfo ':all';
use Date::Calc ':all';
my $findzone = (shift @ARGV);
my $zones = Time::ZoneInfo->new();
foreach my $zone ($zones->zones) {
if ((index uc($zone), uc($findzone)) != -1) {
$ENV{TZ} = $zone;
($year,$month,$day,$hour,$min,$sec, $doy,$dow,$dst) =
System_Clock();
print $zone.":";
print $year."-".$month."-".$day." ";
print $hour.":".$min.":".$sec."\n";
}
}
Note the use of the IF statement. We uppercase both the zone and the
findzone variables to help ensure a match (if "asia" is entered, it will
still match all "Asia" entries). For example, running the script with this
entry:
./tztest.pl pacific
displays the following:
Pacific/Easter: 2002-12-29 23:40:20
Pacific/Galapagos: 2002-12-29 22:40:20
Pacific/Yap: 2002-12-30 14:40:20
Pacific/Truk: 2002-12-30 14:40:20
Pacific/Ponape: 2002-12-30 15:40:20
Pacific/Kosrae: 2002-12-30 15:40:20
Pacific/Tarawa: 2002-12-30 16:40:20
Pacific/Enderbury: 2002-12-30 17:40:20
Pacific/Kiritimati: 2002-12-30 18:40:20
Pacific/Majuro: 2002-12-30 16:40:20
Pacific/Kwajalein: 2002-12-30 16:40:20
Pacific/Auckland: 2002-12-30 17:40:20
Pacific/Chatham: 2002-12-30 18:25:20
Pacific/Tahiti: 2002-12-29 18:40:20
Pacific/Marquesas: 2002-12-29 19:10:20
Pacific/Gambier: 2002-12-29 19:40:20
Pacific/Johnston: 2002-12-29 18:40:20
Pacific/Midway: 2002-12-29 17:40:20
Pacific/Wake: 2002-12-30 16:40:20
Pacific/Honolulu: 2002-12-29 18:40:20
Suppose that the user doesn't know what time zone he/she is looking
for. For example, Pacific/Honolulu covers Hawaii, but isn't necessarily
intuitive to someone looking for "Hawaii." To help the user, we will add
"ALL" as a valid search that returns all zones. To do so, we doctor the
IF statement accordingly:
if (((index uc($zone), uc($findzone)) != -1) ||
("uc($findzone)" eq "ALL" )) {
Now, if the user enters "ALL" for the search, he/she will get all known
zones.
if ( "$findzone" eq "" ) {
# No time zone specified; display input card
print <<INPUTCARD
<do type="accept">
<go href="timezone.pl?findzone=\$findzone"/>
</do>
<p>
Enter zone text to search<br/>
for or ALL for all zones:
<input name="findzone" maxlength="40" format="*A"/>
INPUTCARD
} else {
$findzone =~ tr/ /_/;
$findzone =~ tr/+/_/;
# Set up zones
my $zones = Time::ZoneInfo->new();
my $matches = 0;
print "<p>\n";
# Run through zones, looking for match
foreach my $zone ($zones->zones) {
Simple Applications
Not all wireless applications have to be super-applications. Some of
the best wireless applications perform simple tasks to improve wireless
functionality. The last few articles in this series have shown how
simple, single-purpose gadgets can boost the functionality of mobile
devices. This article will present a slightly more complex application in
The Application
This article will cover how to build a simple phone message
application. Although we live in a time of portable phones, intelligent
voicemail, and other electronic telephone magic, there are still times
when messages are taken by one person (operator) and passed to
others (recipients). For example, consider a businessman who often
travels outside the home office. Many of his customers and contacts
may occasionally call the home office and leave messages with his
secretary. Using a simple Web form, the secretary can pass the
message to the businessman's cell phone, where he can review the
message and even return the call with the simple press of a button.
Application Specifications
This application will utilize the following components:
A simple HTML form to input the message
A flat-file database to store the messages
A CGI script to access the database
Essentially, the application operates as shown in the following
diagram:
Database Design
Our "database" will be a simple delimited flat file. Although we could
go the fancy route with an actual database format, the delimited
</body>
</html>
Note that we include an extra field, "cmd." This hidden field will be
passed to our CGI script to tell it what to do; namely, "save" the data.
$count = 0;
if (-e "msgfile.lock") {
select(undef,undef,undef,0.1);
$count++;
if ($count = 10) { die "Can't open message file! </body></html>"; }
}
open LOCK, ">msgfile.lock";
# Write new record to end of file
open FILE, ">>msgfile.txt";
print FILE $date ." ". $time ."||";
print FILE $from ."||". $message ."||". $callback;
print FILE "\n";
close FILE;
close LOCK;
unlink "msgfile.lock";
# All done, close response page
print "</body></html>";
Because the form and the mobile device could both be utilizing the
script (and hence, the database) simultaneously, we must use file
lockingto avoid having two processes accessing the database and
corrupting our data. We'll use a simple method: creating a file to lock
the database_if the file exists, the process waits for it to be deleted
before accessing the database. When a process is done with the
database, the lock file is deleted and other processes are allowed to
access the database. Instead of failing right away if the file is locked,
or waiting forever for the file to be unlocked, the code loops 10 times,
waiting a tenth of a second between the iterations. If the file is still
locked, we assume that something has gone wrong and exit with an
appropriate error message.
Windows users: Some implementations of Perl on the Windows
platform don't support the four-argument call of "select" used in our
file-locking loop. If this code generates errors on a Windows system,
substitute another delay function.
Listing the Records on the Mobile Device
The following code fragment comprises the code to list five records on
the mobile device, along with an option to move to the next five
records:
Listing: phonemsg.pl - List records fragment
#Pass WML header
print header(-type=>'text/vnd.wap.wml');
# Print WML header and beginning tags
print <<ENDHEADER;
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card>
ENDHEADER
# Print start of card
print <<BEGINCARD;
<p mode="nowrap">
Call List<br/>
<select name="Display" title="Display">
BEGINCARD
# Check file lock
$count = 0;
if (-e "msgfile.lock") {
select(undef,undef,undef,0.1);
$count++;
if ($count = 10) {
die "Can't open message file! </select></p></card></wml>";
}
}
open LOCK, ">msgfile.lock";
# Read records into array
open FILE,"msgfile.txt";
push(@lines,$_) while (<FILE>);
close FILE;
close LOCK;
unlink "msgfile.lock";
# Nextitem = first item to display
$nextitem = param('nextitem');
if ("$nextitem" eq "") { $nextitem = 0; }
# Lastitem = last item to display
Most of the work for this fragment happens in the last quarter of the
code. After the stage has been set (WML card defined), the database is
read into an array and then the correct set of records is displayed, five
records at a time. Initially, the first five records are displayed and an
option is set for the next five to be selected (through the "nextitem"
variable). If the user picks the "Next" option, the script is called again,
with the name/value pair "nextitem" set to the "next item" to display.
Each record is displayed as an <option> whose "onpick" attribute
results in the script being called with the record number to display.
(See the next section for the record-display format and the whole
script section for details on the format and use of the $cmd variable.)
The file-locking routine for the database differs only in the message
that's displayed if a lock cannot be achieved. The message
incorporates enough WML code to complete the card, ensuring that the
user gets the failure message instead of a WML compile error.
Displaying a Specific Record on the Mobile Device
When the user picks a record from the laundry list of records, the
script needs to be able to display the record's details. The following
code takes care of that activity:
Listing: phonemsg.pl - Display specific record fragment
# What record to display
$rec = param('rec');
#Pass WML header
print header(-type=>'text/vnd.wap.wml');
# Print beginning of WML file
print <<ENDHEADER;
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www.wapforum.org/DTD/wml_1.1.xml">
<wml>
<card>
ENDHEADER
print "<p mode=\"wrap\"> \n";
# Check file lock
$count = 0;
if (-e "msgfile.lock") {
select(undef,undef,undef,0.1);
$count++;
if ($count = 10) { die "Can't open message file!</p></card></wml>"; }
}
open LOCK, ">msgfile.lock";
# Load records into array
open FILE,"msgfile.txt";
push(@lines,$_) while (<FILE>);
close FILE;
close LOCK;
unlink "msgfile.lock";
# Split record into fields
($datetime,$from,$message,$callback) = split /\|\|/,$lines[$rec];
# Display record with "call"
# and "call & delete" options
print <<MESSAGE;
$datetime<br/>
$from<br/><br/>
$message<br/>
Callback Number:<br/>
$callback<br/>
<a href="wtai://wp/mc;$callback" title="Call">
Call</a>
<a href="./phonemsg.pl?cmd=callNdel&number=$callback&rec=$rec"
title="CallnDel">
Call & Del</a>
</p>
</card>
</wml>
MESSAGE
The record number to display is passed to the script via the "rec"
variable. As in the other cases, the file lock is checked, the database is
locked, the database is read into an array, and then the database is
released (unlocked). The required record is then read from the array,
unpacked into fields (based on the delimiter), and displayed.
Notice that two links are placed at the bottom of the displayed record:
"Call" and "Call & Del(ete)". The first simply uses the "call this
number" URL format to make the mobile device dial a number. The
latter option allows the user to delete the message before placing the
call, since the message is being returned and should no longer be
stored in the database as an open message.
Placing a Call and Optionally Deleting a Record
The script handles returning a call by including a link to a URL
containing the number to call (see the previous section). Deleting a
record before the call is slightly more complex, and is handled by the
following code:
Listing: phonemsg.pl - Place call/delete record fragment
# What number to call and what record to delete
$number = param('number');
$rec = param('rec');
#Pass WML header
print header(-type=>'text/vnd.wap.wml');
close LOCK;
unlink "msgfile.lock";
# Close card
print "</p> \n </card> \n </wml>";
To delete and call a number, the script is passed the number and the
record number to delete. The number could be retrieved from the
record before the deletion, but is passed separately so it doesn't have
to be parsed from the record. This design decision falls into the "halfa-dozen versus six" category of decisions; I opted to pass the number
we already have, saving the lines required to decode the record before
deleting it.
To perform the deletion, we read the entire file into an array and then
reconstruct the file by writing all records except the deleted record
back to the file.
The Completed Script
After adding a few declaration lines, the controlling structure using the
$cmd variable, and some connecting tissue, our completed script
becomes the following:
Listing: phonemsg.pl
#!/usr/bin/perl
# Include modules
use CGI qw(:standard);
use Date::Calc qw(:all);
# Set command to execute; default = list
$cmd = param('cmd');
if ("$cmd" eq "") { $cmd = "list"; }
# Set current time/date
($year,$month,$day) = Today();
($hour,$min,$sec) = Now();
$time = $hour.":".$min;
$date = $month."/".$day."/".$year;
#
# Call from HTML form, save the data, and return to form
if ("$cmd" eq "save") {
ENDHEADER
# Print beginning of card
# (Number is dialed in 10 secs)
print <<BEGCARD;
<onevent type="ontimer">
<go href="wtai://wp/mc;$number" />
</onevent>
<timer name="delay" value="100"/>
<p mode="wrap">
Deleting record $rec, preparing to call...
BEGCARD
# Check file lock
$count = 0;
if (-e "msgfile.lock") {
select(undef,undef,undef,0.1);
$count++;
if ($count = 10) { die "Can't open message file!</p></card></wml>"; }
}
open LOCK, ">msgfile.lock";
# Read records into array
open FILE,"msgfile.txt";
push(@lines,$_) while (<FILE>);
close FILE;
# Print all records (except deleted)
# back to file
open FILE,">msgfile.txt";
for ($i = 0; $i < @lines; $i++) {
if ($i ne $rec) {
print FILE $lines[$i];
}
}
close FILE;
close LOCK;
unlink "msgfile.lock";
# Close card
print "</p> \n </card> \n </wml>";
}
Note that the default action of the script is to list the records. This
allows the mobile device to call the script without arguments
("http://URL/phonemsg.pl") to get the ball rolling. Subsequent calls
are handled by the script ("display," "callNdel," etc.) where it controls
the parameters, saving the mobile user from having to
enter/bookmark them.
To test the application, we seed the database with the following data:
Listing: msgfile.txt - Sample data
1/25/2003
555-1212
1/25/2003
555-1212
1/25/2003
555-1212
1/25/2003
555-1212
1/26/2003
555-1212
1/26/2003
555-1212
1/26/2003
555-1212
1/27/2003
555-1212
1/27/2003
555-1212
1/28/2003
555-1212
1/28/2003
555-1212
1/28/2003
555-1212
1/28/2003
555-1212
1/28/2003
555-1212
1/28/2003
555-1212
1/29/2003
555-1212
1/29/2003
555-1212
1/29/2003
555-1212
12:15||Caller Number01||Sample message, from sample caller.||31712:25||Caller Number02||Sample message, from sample caller.||31712:35||Caller Number03||Sample message, from sample caller.||31713:15||Caller Number04||Sample message, from sample caller.||31714:15||Caller Number05||Sample message, from sample caller.||31714:23||Caller Number06||Sample message, from sample caller.||31715:15||Caller Number07||Sample message, from sample caller.||3179:15||Caller Number08||Sample message, from sample caller.||3179:35||Caller Number09||Sample message, from sample caller.||3178:05||Caller Number10||Sample message, from sample caller.||31710:15||Caller Number11||Sample message, from sample caller.||31711:11||Caller Number12||Sample message, from sample caller.||31712:01||Caller Number13||Sample message, from sample caller.||31714:15||Caller Number14||Sample message, from sample caller.||31716:45||Caller Number15||Sample message, from sample caller.||3178:25||Caller Number16||Sample message, from sample caller.||3179:04||Caller Number17||Sample message, from sample caller.||31710:35||Caller Number18||Sample message, from sample caller.||317-
1/29/2003
555-1212
1/30/2003
555-1212
1/30/2003
555-1212
1/30/2003
555-1212
10:39||Caller Number19||Sample message, from sample caller.||31712:15||Caller Number20||Sample message, from sample caller.||31715:02||Caller Number21||Sample message, from sample caller.||31716:05||Caller Number21||Sample message, from sample caller.||317-
FIGURE 4 - Two links at the bottom of the record allow the user to call
and optionally delete the message.
Images are courtesy Openwave Systems Inc. (Openwave, the
Openwave logo, Openwave SDK, Openwave SDK Universal Edition,
Openwave SDK WAP Edition are trademarks of Openwave Systems
Inc. All rights reserved.)
However, it didn't take long for the business side of the Web to realize
the potential of cookies and begin using them for their own purposes
tracking user activities, shopping habits, and other bits of personal
information.
Shortly thereafter, the media and user advocates led the charge
against cookies, causing most modern browsers to offer the option of
refusing to store any cookies, or allowing the user to choose when a
cookie should or should not be stored. Unfortunately, refusing cookies
causes problems for Web sites that use cookies to store user
preferences and login info, requiring the user to reenter such info on
each visit.
Note: I don't condone the illicit use of cookies for tracking users'
personal information, but do recognize the utility of storing frequently
used information to aid the user experience.
How Cookies Work
Cookies work by storing data on the user's local computer/device. This
data is stored via an HTTP dialog between the server and the client.
That data can then be recalled by the server, processed, updated, etc.
Figure 1 shows the data paths associated with storing and retrieving
cookies.
Notice the use of a semicolon (;) to delimit the parameters. The date
is in the format "weekday, dd/Mon/yy hh:mm:ss." In the example
above, the cookie will expire at one second before midnight on
Monday, March 24, 2003, Greenwich Mean Time.
Tip: It's important to include at least one blank line after the SetCookie header and before the WML headers so the client correctly
identifies the WML headers.
Let's look at a real example of using Perl to set a cookie for a WML
deck. The following code snippet shows how Perl is used to set a
cookie and output a status ("Cookie set") message:
#!/usr/bin/perl
# Define minimal deck
$deck = '
<wml>
<card>
<p>
Cookie set.
</p>
</card>
</wml>';
# Send Content-type and Set-Cookie headers
print "Content-type: text/vnd.wap.wml \n";
print "Set-Cookie: name=Steve \n";
# Send WML header info
print "\n<?xml version=\"1.0\"?>\n";
print "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\""
. " \"http://www.wapforum.org/DTD/wml_1.1.xml\">\n";
# Send deck
print $deck;
The cookie in the code above was set without a TTL. To set an
expiration date, we draw on the Date::Calc module to do our date
calculations:
#!/usr/bin/perl
# Include Date::Calc.
use Date::Calc':all';
# Get today in GMT
($year,$month,$day) = Today([$gmt]);
# Add a year (365 days)
($year,$month,$day) =
Add_Delta_Days($year,$month,$day,"365");
# Get textual representations of month and day of week
$dow = Day_of_Week_to_Text(Day_of_Week($year,$month,$day));
$month = Month_to_Text($month);
# Make sure day is two digits
if ($day<10){
$day = '0'.$day;
}
# Assemble expiration date
$date = $dow.", ".$day."-".$month."-".$year." 23:59:59 GMT";
# Define deck
$deck = '
<wml>
<card>
<p>
Cookie set.
</p>
</card>
</wml>';
# Send Content-type and Set-Cookie headers
print "Content-type: text/vnd.wap.wml \n";
print "Set-Cookie: name=Steve; expires=$date; \n";
# Send WML headers
print "\n<?xml version=\"1.0\"?>\n";
print "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\""
. " \"http://www.wapforum.org/DTD/wml_1.1.xml\">\n";
# Send the deck
print $deck;
Reading cookies with Perl is even easier than setting them, thanks to
the environment variable HTTP_COOKIE. This variable contains
name/value pairs for all applicable cookies (those matching the current
scope).
To parse the name/value list, you could use the following code:
@nvpairs=split(/[,;] */, $ENV{'HTTP_COOKIE'});
foreach $pair (@nvpairs) {
($name, $value) = split(/=/, $pair);
$cookie{$name} = $value;
}
This code effectively parses the cookie list into the array $cookie,
where each value can be accessed by its name. For example, our
earlier "name" example would yield:
$cookie{'name'} = "Steve"
If the cookie was still set, this code would display the following:
Cookie (name) = Steve
Deleting Cookies
date/time that has already passed. For example, we could use the
code earlier but subtract a day or two to expire the cookie. The code,
using Date::Calc functions, would resemble the following:
# Get today in GMT
($year,$month,$day) = Today([$gmt]);
# Subtract a year (365 days)
($year,$month,$day) =
Add_Delta_Days($year,$month,$day,"-365");
# Get textual representations of month and day of week
$dow = Day_of_Week_to_Text(Day_of_Week($year,$month,$day));
$month = Month_to_Text($month);
# Make sure day is two digits
if ($day<10){
$day = '0'.$day;
}
# Assemble expiration date
$date = $dow.", ".$day."-".$month."-".$year." 23:59:59 GMT";
Alternatively, you could encode all this data into one lengthy string.
My advice, however, would be to tie one unique piece of data that
identifies the user to a database record. Basically, store only what you
need on the client device, and store the rest of the data in a database
accessible by the server. The flowchart for an application using this
method might resemble Figure 2.