Learning WML - WAP Basics By Steve Schafer This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This first article covers the background of WAP, how it works, and what you need to get started. Future articles will introduce the WML language, tips and tricks to provide content, and how to integrate other technologies such as PHP to make your pages more flexible. 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. A Brief History of WAP As previously stated, WAP refers to a wide range of technologies and protocols, all related to mobile Internet functionality. This functionality has roots dating back to the mid 1990s. At that time, several vendors were working on the mobile Internet problem as mobile device sales skyrocketed, and several competing technologies emerged: • Nokia's Narrow Band Sockets (NBS) and Tagged Text Markup Language (TTML) • Ericsson's Intelligent Terminal Transfer Protocol (ITTP) • Unwired Planet's Handheld Device Markup Language (HDML) Each technology had its own purpose, but some overlapped with others in various areas. This diversity threatened to fragment the wireless industry along provider lines. In mid 1997, the WAP Forum was founded to aid in communication among the developers and to spur a common set of protocols and technologies. In the same year, the industry took another step forward with the formation of the Open Mobile Alliance (OMA), which combined several distinct development and standards bodies into one. How Does WAP Work? These articles will focus on the delivery of WML content to mobile devices over a cellular or related technology network. However, the delivery of many protocols and technologies takes the same routenamely, through a proxy server that bridges the gap between the wired Internet and the wireless service provider's network. Figure 1.1 The WAP Gateway provides wireless networks with Internet access and optional content translation and filtering. This proxy server manages the communication between the wireless client and the Internet server(s), acting as a gateway to the wired Internet. It caches content and in some cases even translates raw HTML into WAP-compatible protocols such as WML. Many mobile devices have a built-in wireless browser. Although several different browsers are in use today among the various wireless providers, most browsers support WML, either natively or translated into HDML. A popular precursor to WML, the Handheld Device Markup Language (HDML), is still supported on several mobile platforms. However, due to the limitations of HDML (supporting only a handful of navigation tags and virtually no formatting tags), WML is becoming the most widely used mobile markup language. That said, if you plan to support a particular platform, it's best to test your code extensively on that particular device. Note: When coding for the general public, be careful to stick to the standards and avoid using proprietary extensions to the various languages, no matter how tempting the feature set of the extensions. If you decide to provide the extensions to those who can use them, you should take the necessary server steps to identify the connecting browser and deliver code customized for that browser. 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.2 - The physical card analogy to WML decks helps visualize how they work. These cards together form a deck and are delivered to the mobile device in one file. Now suppose that each card links to the next (card one links to card two, which links to card three, and so on), and that each card also has a "back" link to take the user back to the previous card. As the user navigates the deck, the cards stack in memory 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 whole deck before redisplaying the card-a card it already had in memory. Instead, you should use the tag, which tells the browser to remove the current page and display the previous page in the history list (like using the Back button on a PC browser). Of course, the content of the previous page might need to be refreshed each time it's accessed; in that case, valid techniques could include recalling the whole deck or specifying that the page not be cached. Proper navigation will be covered in future articles. 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. Setting Up Your Server for WML To configure your Web server to deliver WML, you must define the related MIME types for WML content. Web servers and client browsers use MIME (Multipurpose Internet Mail Extensions) to communicate the type of data that is being sent. Before sending data, the server sends a MIME identifier to the client browser, identifying the format of the following data. The client browser can then properly decode and apply the data. Most WML applications require three MIME types, as listed in the following table. File Extension MIME Type Definition Use .wml text/vnd.wap.wml WML source file .wmls text/vnd.wap.wmlscript WML script file .wbmp image/vnd.wap.wbmp Wireless bitmap file (image) 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. A Sample WML Deck Now that your server is set up to handle WML correctly, let's try serving up a sample page. The following listing shows the bare minimum coding necessary to contain a WML deck, consisting of a single, blank card: <?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="Sample "> </card> </wml> 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> Place the file in an accessible directory with adequate permissions to access it from an external browser. Now visit the Wapalizer at http://www.gelon.net. Type the URL to the file in the Wapalizer box and click on the Wapalize button. You should see a screen similar to the output below. Figure 1.5 - Your sample page in the Openwave simulator. 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.) Learning WML - Tools and Structure This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers some essential tools for coding and debugging WML code, as well as the basics of creating WML decks. Future articles will cover advanced WML language, tips and tricks to provide content, and how to integrate other technologies such as PHP to make your pages more flexible. Note: These articles cover WML version 1.1, which is supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. Essential Documentation and Tools There are several tools you should assemble before beginning to code your WML applications. Note: The recommendations in this article are simply that: recommendations. You can make do with a simple text editor (such as Windows Notepad or vi in Linux) and the Web server, but spending some time assembling some good tools will pay off in the long run. Tip: If you decide not to use one of the WAP SDKs/IDEs and use Windows as your OS, try TextPad, a nifty text editor that's very versatile and code friendly (http://www.textpad.com). 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. Open Mobile Alliance - http://www.openmobilealliance.org The Open Mobile Alliance (OMA) is a relatively new standards organization made up of several key players in the WAP arena, including most cellular providers and phone manufacturers - visit http://www.openmobilealliance.org/members.html for a full list. The new site is still evolving, but much of the content from the previous site (the popular "WAP Forum") is still available. As of this writing, you can access the documentation by using the "Access to WAP specific information" link at the bottom of the main OMA page. A full list of WML release specifications, WAP specifications, and DTD definitions is available. Openwave Developer Program http://developer.openwave.com Openwave, the company behind the popular Openwave Mobile Browser (formerly the UP.Browser) maintains a library of technical documents and developer guidelines. Follow the links on the left side of the main page to access WML language references (in the Technical Library), UI guidelines, lists of supported devices, and more. Forum Nokia - http://www.forum.nokia.com Nokia was one of the pioneers in the WAP arena and maintains an impressive number of emulators and related documentation. Their Documents section has a handy search feature to help find the document most applicable to your needs. Emulators and SDKs You could test your WML code with a cell phone, but most providers charge a premium for Web access, and testing could get rather expensive. Instead, use one of the following emulators, some of which are packaged with a full SDK_some even include a full Integrated Development Environment (IDE). Note: Most of the emulators and SDKs are only available for Windows, although a few are available for Solaris and other operating systems. 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). Basic WML Structure and Rules Now that we have our server set up and our toolkit stocked, we're almost ready to do some coding. First let's examine the basic form of WML documents and some basic coding rules. Basic WML Deck Structure The following listing shows the basic elements you need in most WML decks: <?xml version="1.0"?> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> <wml> <head> <meta.../> </head> <card id="cardid1" title="Card1"> ... </card> <card id="cardid2" title="Card2"> ... </card> ... </wml> 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 example, within a element, the following must appear in order: <onevent>, <timer>, <do>. WML Text Formatting Like HTML, WML supports several types of text formatting. The following table describes the available text-formatting tags. Tag Name <p> Paragraph <br/> Line break program. <b> Bold <big> Big <em> Emphasized <i> Italic <small> Small <strong> Strong size and italic.) <u> Underline 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> It results in the following display: Figure 2-1 - The mode attribute determines how the tag wraps text. 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.) 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 solid, it isn't a gas, but it can be broken. What is it? A: Silence. We'll put the question in one card, the answer in another, and define links to move between the two. For the sake of variety, let's define the link forward (to the answer) as a button and the link backward (back to the question) as an in-text link. Consider 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="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> <prev></prev> Back to Question </anchor> </p> </card> </wml> 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> The label attribute supplies an optional label to the Accept button when the link is selected. We've omitted this attribute in the example, allowing the default "Link" (or other device default) to be displayed. The type of anchor can be <go>, <prev>, or <refresh> - you simply add the appropriate element to define the type. We've used <prev> to return to the previous card. The text is the actual text shown for the link, in this case "Back to 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 the <prev> command to the Accept key - is superfluous. Most devices incorporate a standard "Back" button that could be used to return to the question without the overhead of remapping a key. With that in mind, let's put the key to a better purpose: moving to another question. Change the <anchor> to the following: <anchor> <go href="#card3" /> Next Question </anchor> 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"> <do type="accept" label="Answer"> <go href="#card4" /> </do> <p mode="wrap"> What is greater than God, more evil than the devil, poor people have it, rich people want it, and if you eat it you will die? </p> </card> <card id="card4" title="Answer"> <p mode="wrap"> Nothing. Nothing's greater than God, nothing's more evil than the devil, poor people have nothing, rich people want nothing more than they already have, and if you eat nothing you will die. </p> <p mode="nowrap"> <br/><br/> <anchor> <go href="#card5" /> Next Question </anchor> </p> </card> </wml> Note: Because the answer to the second question is lengthy, I moved the <anchor> to its own nowrap paragraph, as discussed earlier. Also, I set up the last link to a nonexistent "card5," assuming that we would add more questions and answers. Understanding Device Keys Most mobile devices have a limited number of keys to aid in navigation. Many have a default Back key (my Samsung cell phone uses the CLR key), as well as Accept and Options keys (my Samsung uses OK and MENU, respectively). Note: See the WAP documentation for a particular device to determine which keys perform which default purposes. 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. Learning WML - Navigation, User Input, and Graphics This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers more advanced navigation, accepting user input, and displaying graphics. Future articles will cover advanced WML language, tips and tricks to provide content, and how to integrate other technologies such as PHP to make your pages more flexible. Note: These articles cover WML version 1.1, which is supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. More Navigation Between Cards and Decks In the previous article ("Learning WML - WML Tools and Coding Basics"), we covered basic navigation using <anchor> and <do> elements. These basic navigation constructs enable you to map links to the soft keys or place standard HTML-style hyperlinks into your cards. There are a few more useful navigation methods we should also cover. Select Lists The select list provides an easy way for the user to select between several predetermined values. Each value is enclosed within an <option> tag, much like an HTML list: <select> <option>First option</option> <option>Second option</option> </select> Each option is automatically assigned a shortcut number, displayed next to it by the browser, as shown in the following figure. Each <option> element is assigned a shortcut number which is displayed next to the element's text. 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.) The user can either press the shortcut number associated with the option or use the standard navigation (arrow) keys to select the appropriate option and then press the Accept key. Tip: The shortcut keys are only useful if you have 10 or fewer options, since there are only 10 numeric keys (0-9) that can be used to quickly select an option. If you have more than 10 options, consider adding a "More" option that displays the options in groups of 10 or fewer. The "onpick" parameter can be used to select a deck/card to navigate to if the user selects that option. For example, the following code will navigate the user to the appropriate "color" card within the current deck: <select> <option onpick="#red">Red</option> <option onpick="#green">Green</option> <option onpick="#blue">Blue</option> </select> A full deck example follows: <?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="Select example"> <p> <select> <option title="Red" onpick="#red">Red</option> <option title="Green" onpick="#green">Green</option> <option title="Blue" onpick="#blue">Blue</option></select> </p> </card> <card id="red" title="Red"> <p> Your choice was Red. </p> </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. Learning WML - Navigation, User Input, and Graphics Events You can also set up navigation that is caused by a particular event. The <onevent> element provides several possible actions triggered by one or more intrinsic events. The <onevent> tag has the following format: <onevent type="type"> task </onevent> The "type" can be one of the following: 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 The "onenter" types can be used to display or omit particular information depending on how the user navigated to the card, or to force a refresh of dynamic information if the user reached the card by going backward (thereby getting the card from the cache and not a fresh copy from the server). The "onpick" type is used when the <onevent> is nested within an <option> element to provide an action when the option is selected. The "ontimer" type is used to provide a task at a particular time, independent of user actions (refreshing a page, moving to another page, etc). The various tasks that can be used with the <onevent> element are described in the following table. Task Use <go> Navigates to a new card. <prev> Navigates to the previous card. <noop> Performs no task (no operation). <refresh> Refreshes the current card's content. <exit> Exits the current context. * <spawn> Spawns a child task. * <throw> "Throws" an exception that can be caught by an optional <catch> element. * * These tasks are specific to the Openwave browser. See the developer documentation at http://developer.openwave.com for more information. The "ontimer" event can be used to display a splash screen (containing welcoming or copyright information), or to automatically return to a card after displaying an error. It_s also very useful for refreshing a card to display up-to-date dynamic data. 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> <card id="main" title="Main Menu"> <p>Main menu here...</p> </card> </wml> Note: The <timer> element's value parameter is measured in tenths of a second, so our splash screen stays active for 5 seconds (50 tenths). Tip: The <card> element includes an optional "ontimer" parameter, which alleviates the need for a separate <onevent> element. Using this parameter, the above example could be shortened as follows: ... <card id="splash" title="Welcome!" ontimer="#main"> <timer name="delay" value="50"/> Accepting User Input Although the forms capability of WML isn't as robust as standard HTML, the <input> element is very flexible and allows a variety of data to be accepted from the user. The <input> element has the following minimal form: <input name="variable_name" /> 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 Displays the specified number (0-9) next to the element. The key can be used to quickly select the element. Specifies the format mask of the input. See the format section below for details on masking. Controls whether the element can be left empty. The valid values for this parameter are "true" (the default) or "false." Maximum length of the input, in characters. Note: The "accesskey" parameter allows you to create a form of several input fields where the user can easily navigate between the fields. However, as the Openwave applications point out (http://developer.openwave.com), wizards that display card-sized chunks of data entry fields are much better received and make a better application design. The "format" parameter's value is composed of a series of characters that provide a mask for the input. The following table describes the valid instructional characters for the format mask: Character Meaning A Any symbol or uppercase alpha character (no numbers). A Any symbol or lowercase alpha character (no numbers). N Any number (no symbols or alphabetic characters). X Any symbol, number, or uppercase alpha character (cannot be changed to lowercase alpha). X Any symbol, number, or lowercase alpha character (cannot be changed to uppercase alpha). M Any symbol, number, or alpha character; by default, the first character is uppercase. M Any symbol, number, or alpha character; by default, the first character is lowercase. The format mask can also include symbols, which will be displayed in their appropriate position in the input. For example, to accept a phone number, including the area code, you could use this format mask: format = "(NNN) NNN-NNNN" When using the same character multiple times in a row, you can prefix the character with a number indicating how many times it repeats. Using this method, our phone number mask could be written like this: format = "(3N) 3N-4N" However, this format isn't as recognizable as a phone number mask. Note: To specify an unlimited number of the same character, prefix the character with an asterisk (*). A simple deck to accept a person's first name (up to 25 characters) and then greet the person by name would resemble the following listing: <?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="GetName" title="Get Name"> <do type="accept"> <go href="#Hello"/> </do> <p> What is your first name? <input name="name" format="*M" maxlength="25" /> </p> </card> <card id="Hello" title="Hello"> <p> Hello, $(name)! </p> </card> </wml> 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 trigger the navigation. There are many useful things you can do with user-supplied input besides echo it back in other cards. The next article in this series will discuss several other uses for user input. Graphics and Icons The current generation of mobile devices includes very limited graphics capability. Most mobile browsers only support black-and-white bitmapped images, although a few recent browsers have added support for color and even animated formats (GIF, PNG, JPG). Some mobile gateways also perform on-the-fly conversions of graphics, most notably from BMP to WBMP. Tip: Unless you are developing for a particular platform where you know the capabilities, use only small black-and-white WBMP graphics. Most graphics applications do not support the WBMP format, but several converters exist, including a Web-based converter at Teraflops (http://www.teraflops.com/wbmp/). Note: Refrain from using graphics whenever possible; using graphics breaks the mobile application development rules of "fast" and "simple." To create a graphic for mobile use you will need a graphics application that supports the WBMP format, or at least black-and-white bitmap format and a suitable converter. Drawing upon our earlier splash screen example, let's create a graphical logo for ACME. To start, we need to choose a suitable size for the graphic. It's advisable to stick with a small size such as 100 x 100 pixels. The graphic shown in the following figure was created with the following steps (note that the options and procedure might differ in your graphics application): 1 Create a new graphic, sized 80 x 80 pixels. 2 Convert the graphic to black-and-white, or "line art." 3 Select the appropriate background color (black or white) and fill the entire graphic. 4 Use the text tool to create text in appropriate sizes and fonts (using the opposite color of the background). 5 Save the results in GIF format. 6 Use the online tool at Teraflops.com to convert the GIF to WBMP format. 7 Upload the resulting graphic (acme.wbmp) to the Web server. Tip: We advise using a graphics program that supports objects instead of straight bitmapped images. Graphics programs allow you to work with individual objects_moving, sizing, etc_before committing them to the electronic canvas. The ACME Incorporated logo. Images are added to WML cards using the <img> tag. This tag has the following syntax: <img alt="alt-text" src="url" /> 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> <card id="splash" title="Welcome!" ontimer="#main"> <timer name="delay" value="50"/> <p mode="wrap" align="center"> Welcome to<br/> <img alt="ACME Incorporated" src="acme.wbmp" /> </p> </card> <card id="main" title="Main Menu"> <p>Main menu here...</p> </card> </wml> Our new splash screen. 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.) Icons are very small graphics to be used as highlights or ornamentation. Most mobile browsers support at least a limited number of icons. For the purposes of this discussion we will use icons supported by the Openwave browser. A full list of these icons can be found in the WML Language Reference documents on the Openwave developer site (http://developer.openwave.com). Icons are placed as graphics, using the <img> tag and its "localsrc" attribute, along with the appropriate icon number(s). For example, a smiley face is icon number 68; it would be placed in the contents of a card with the following tag: <img alt=":-)" src="" localsrc="68" /> 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" /> Our new splash screen complete with airplane icon. 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.) Tip: Because the WML language is fairly limited for emphasizing text, consider using icons such as triangles (icons 5-8) to draw attention to specific text. For example, this code is used to call attention to the fact that the user has reached the end of an article: <img alt="--" src="" localsrc="righttri1" /> End <img alt="--" src="" localsrc="lefttri1" /> Learning WML - Variables and Scripting This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers variables and beginning scripting with WMLScript. Future articles will cover more advanced scripting and how to integrate other technologies such as PHP to make your pages more flexible. Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. 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. Variables in WML consist of words, enclosed in parentheses, prefixed with a dollar sign ($). For example: $(name) $(address) $(link) $(target_url) $(_method) Note: Because the dollar sign ($) is used to signify a variable, it is a reserved character in WML. If you want an actual dollar sign to appear anywhere in your card(s), use a double dollar sign instead ("$$"). Variable names must start with a letter or underscore. Subsequent characters can be alpha, numeric, or underscores. Variables are case sensitive; "phone_number" is different from "Phone_Number." Wherever a variable is referenced, WML will substitute the value of the variable where the variable name appears. For example, the following code: <p> Hello $(name)! </p> would produce the following result, if "David" was stored in the variable $(name): Hello David! Note: WML reserves the ampersand symbol (&) for entities. To use this symbol in your deck, you must substitute the corresponding entity "&amp;". Setting Variables Variables can be set by the following elements: <input> <select>/<option> <setvar> Each of these elements is discussed in more detail below. Note that some of these elements were introduced in previous articles, and their parameters and such will not be detailed here. The <input> Element The <input> element is straightforward; it accepts user input and stores it in the variable specified. For example, the following code would accept input for a phone number and store it in the variable $(phone_number). The number is then displayed on the second card. <?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="Enter Number"> <do type="accept" label="Next"> <go href="#card2" /> </do> <p> What is your phone number? <input name="phone_number" type="text" format="(NNN) NNN-NNNN" /> </p> </card> <card id="card2" title="Display Number"> <p> Your number is:<br/> $(phone_number) </p> </card> </wml> 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" /> The use of <setvar> is complicated by the fact that it can only be executed upon a <go>, <prev>, or <refresh> action. As such, you cannot simply embed a <setvar> anywhere you need to set a variable. It is common practice to set variables before moving to a card where you will need them, by embedding the proper <setvar> elements within the corresponding <go> or other action element. If you need to set variables for the current card and doing so from a previous page is impractical (for example, for the first card displayed in the deck), you can use <onevent> and <refresh> elements to force a <setvar> to execute: For example, the following code will set the 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> The <go> element can include an optional "method" parameter that can be set to "get" or "post," which determines how the variable/value pairs are sent to the server. There are many differences between the two methods, but generally speaking "get" encodes the pairs in the calling URL while "post" sends the pairs in the body of the calling message. Of the two, "post" should be used wherever possible because it is more robust. Using this method, you can pass values to CGI scripts or other external applications. For example, the following example passes the name/value pair color/Red to colorselect.cgi: <go href="colorselect.cgi"> <postfield name="color" value="Red" /> </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) /> As flexible as variables are, they only serve a static purpose on their own. Although you can store a variety of information, no decisionmaking ability is provided to set variables or make decisions regarding the status of variables within standard WML. However, you can extend the use of variables by using WMLScript to complement your WML code. Beginning WMLScript Programming WMLScript provides a means to add basic scripting to WML. WMLScript is similar to JavaScript-knowing JavaScript will certainly help you learn WMLScript. However, if you have any programming experience you should be able to learn WMLScript and be coding in no time. Note: WMLScript does not translate to HDML. If you are creating pages that might be delivered to an HDML-compatible device (usually through a translation gateway), avoid using WMLScript. Basic WMLScript Concepts Although a full primer on WMLScript is outside the scope of these articles, it is important to understand a few basic concepts. • WMLScript is case sensitive. All language elements must be spelled using the proper capitalization of letters. • WMLScript supports integer, floating point, string, and Boolean literals. String literals must be enclosed in single or double quotes (' ' or " ") and Boolean values are set to "true" or "false." • WMLScript will automatically convert between values to deliver the right type of value (integer, string, etc.) to the function that receives the value. • Whitespace is ignored in WMLScript, allowing the programmer to format the code as desired. • Comments in WMLScript can consist of a single line or multiple lines. Single-line comments start with a double slash (//). For multiple-line comments, start the block with a slash followed by an asterisk (/*) and end the block with an asterisk followed by a slash (*/). • As with any language, WMLScript has a handful of reserved words such as "function," "var," "continue," etc. For a full list, consult the language guide for the version of WMLScript you are using. • You must declare all variables in WMLScript. • WMLScript can access variables set in WML. You can set a variable in a WML card and reference that same variable in a WMLScript. You can also pass values to WMLScript functions and return values from the function to the calling WML. • WMLScript is contained in external files (usually with a WMLS extension) and called from standard WML navigation elements using the following expression: <script filename>#<function name>(<arguments/parameters>) For example, to call the function "validate" in the file "validation.wmls" you would use validation.wmls#validate() Your First Script The easiest way to learn a scripting language is by example. The following basic example accepts a number, multiplies it by 2, and displays the result. This is a relatively useless script, but it helps simply to illustrate the mechanics between WML and WMLScript. This example uses the following two files: sample.wml 1. <?xml version="1.0"?> 2. <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> 3. <wml> 4. <card id="card1" title="Enter Number"> 5. <onevent type="onenterforward"> 6. <refresh> 7. <setvar name="num" value=""/> 8. </refresh> 9. </onevent> 10. <p> 11. <do type="accept" label="Multiply"> 12. <go href="sample.wmls#multiply()"/> 13. </do> 14. <input name="num" format="*N" /> 15. </p> 16. </card> 17. 18. 19. 20. 21. 22. <card id="card2" title="Result"> <p> Your number ( $(num) ) multiplied by 2 is:<br/> $(numX2) </p> </card> 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. A More Useful Script One handy purpose for WMLScript is validating input and acting accordingly. For example, suppose we had a card that asked for a twoletter U.S. state abbreviation. Although the <input> element can 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. More Information on WMLScript A wealth of information can be found at the Openwave developer site, http://developer.openwave.com. Take a look at their Technical Library and be sure to visit the Developer Forum for answers to more difficult problems. WML Scripting Tips and Integration with PHP This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers some additional uses for WMLScript and how to integrate PHP with WML. Future articles will cover more advanced PHP and WML techniques. Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. 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"); } } The deck (animation.wml) sets two variables and forces a refresh. These are the variables: num: The number to append to the image source image: The starting image source (image1.wbmp) Every two seconds, the main() function in animation.wmls is called. This function adds one to "num," creates a new image source (image<num>.wbmp), and refreshes the card, which causes the new image to be displayed. After the ninth image (image9.wbmp) is displayed, the script goes to the second card in the deck, ending the animation. Note: The above example assumes that your animation consists of nine images that, when displayed sequentially, create the animation. WMLScripting is flexible and brings basic scripting to WML. However, it's possible to lend more programming power to WML by infusing it with PHP, as covered in the next section. WML Scripting Tips and Integration with PHP Integrating PHP into WML Although WML is well suited to most mundane content delivery tasks, it falls short of being useful for database integration or extremely dynamic content. Another Web technology, PHP, fills this gap quite nicely-integrating into most databases and other Web structures and languages. It's possible to "cross-breed" mime types in Apache and IIS to enable PHP to deliver WML content. The following sections show you how to configure your server and how to integrate the two technologies. Note: This section assumes that you have a basic understanding of PHP. To learn more about PHP, visit the PHP Web site (www.php.net) and browse through the language documentation. You can also find a series of articles here on Developer.com for learning PHP. The first is Learning PHP: The What's and the Why's What Is PHP? According to the PHP documentation, "PHP (recursive acronym for 'PHP: Hypertext Preprocessor') is a widely-used Open Source generalpurpose scripting language that is especially suited for Web development and can be embedded into HTML." Essentially, this scripting language is a server-side language that's processed before being sent to the requesting client. This is in contrast to scripting languages like JavaScript that are client-side and processed by the client's browser after being sent to the client. The benefit of being a preprocessed language is that PHP pages can be highly dynamic. Decisions can be made from user input, databases, server conditions, and so on about how the requested content is delivered to the client. It's exactly what we need to increase the power of our WML. Installing PHP PHP is available for most major Web server platforms, including Apache and Microsoft's Internet Information Server (IIS). Visit www.php.net, download the appropriate version for your server, and install it by following the included instructions. Note: If you're running Debian, Red Hat, or another version of Linux with a robust packaging system, PHP is probably available in a preconfigured package. Check your distribution's Web site and documentation for more info on PHP package(s). Cross-breeding with WML: Apache To enable WML pages to be parsed by PHP, you simply have to add the WML suffix to the PHP application definition in your Apache configuration file: AddType application/x-httpd-Php .php .wml 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; CREATE TABLE Phone ( FirstName varchar(30) default NULL, LastName varchar(30) default NULL, Phone varchar(12) default NULL ); INSERT INSERT INSERT INSERT 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'); Now we'll create a relatively simple PHP program to display the customers in a select list. When the user selects an entry, the phone will dial the selected number. 1. <?php 2. header("Content-type: text/vnd.wap.wml"); 3. print "<?xml version=\"1.0\"?>\n"; 4. print "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"" 5. . " \"http://www.wapforum.org/DTD/wml_1.1.xml\">\n"; 6. print "<wml>\n"; 7. print "<card id=\"Phonebook\">\n"; 8. print "<p><select>\n"; 9. $link = mysql_connect("localhost", "user", "password") 10. or die("Could not connect to database!"); 11. mysql_select_db("customers") 12. or die("Could not select database!"); 13. $query = "select * from Phone order by LastName"; 14. $result = mysql_query($query,$link) 15. or die("Query failed:$query"); 16. 17. 18. 19. 20. 21. while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) { print "<option title=\"Call\""; print " onpick=\"wtai://wp/mc;$line[Phone]\" >"; print "$line[FirstName] $line[LastName] ($line[Phone])\n"; print "</option>\n"; } 22. print "</select></p>\n"; 23. print "</card>\n"; 24. print "</wml>\n"; 25. ?> The code is fairly straightforward: • Lines 1-5 are our standard WML preamble. • Lines 6-8 open the required tags. • Lines 9-12 create the link to the database. Be sure to substitute an actual username and password for the placeholders. • Lines 13-15 query the database for all fields in all rows of the Customers database. • Lines 16-21 step through the results row by row and build an <option> for each one. • Lines 22-24 close all open tags. Each <option> includes an "onpick" attribute that will dial the selected number on most devices, via the "wtai://wp/mc;<phone number>" URL scheme. This is a useful trick for all users to dial numbers from within your WML. The generated output resembles the following figure: 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.) 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). Learning WML -Scripting Tips And Integration With PHP Project Description In our last article we implemented a simple phone list using PHP and MySQL. The basic premise was to give salespeople access to a central contact database without the need of synchronizing their phones/PDAs. The script pulled contact data from a database, displayed the records, and dialed the selected record. This project will expand on that example, adding the following features: • The contact database will include address information. • There will be more contacts, requiring the list to be displayed a few records at a time. • A search feature will be added. Because there will be more than one feature (list and search), we will also need a menu so the user can choose what feature he or she wants. Each screen will also need a way to return to the menu. PHP Script Outline The project will use one PHP script and a controlling variable ($cmd) 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. The Contact List Our contact database will contain the following data: FirstName Jack 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'); INSERT INTO Phone VALUES (3,'Sally','Nash','317-555-8765', '644 Innovation Pl','Ft Wayne','IN','46875','2002-08-22'); INSERT INTO Phone VALUES (4,'Bill','Haskins','317-555-8766', '201 W 103rd','Indianapolis','IN','46240','2002-08-24'); INSERT INTO Phone VALUES (5,'Jill','Payton','317-555-0098', '55 W 96th St','Westfield','IN','46222','2002-08-22'); INSERT INTO Phone VALUES (6,'Mary','Martinez','317-555-7544', '9433 E 75th Ave','Greenwood','IN','46784','2002-08-22'); INSERT INTO Phone VALUES (7,'Ned','Tanner','317-555-9877', '77 E Marchen','Ft Wayne','IN','46875','2002-08-23'); INSERT INTO Phone VALUES (8,'Bruce','Wilten','317-555-1111', '3 Prospect','Indianapolis','IN','46038','2002-08-22'); INSERT INTO Phone VALUES (9,'Naomi','Waters','317-555-4323', '1121 Central Pl','Westfield','IN','46055','2002-08-22'); INSERT INTO Phone VALUES (10,'Angela','Renault','317-555-0988', '5674 E 6th Ave','Noblesville','IN','46234','2002-08-22'); INSERT INTO Phone VALUES (11,'Markus','Elliot','317-555-3232', '9755 Carter','Indianapolis','IN','46250','2002-08-22'); INSERT INTO Phone VALUES (12,'Steve','Albert','317-555-5444', '95 Crescent Dr','Indianapolis','IN','46250','2002-08-26'); INSERT INTO Phone VALUES (13,'Martin','Rolfsen','317-555-6767', '5678 E 7th Ave','Indianapolis','IN','46234','2002-08-22'); INSERT INTO Phone VALUES (14,'Lisa','Biggins','317-555-3644', '7732 Allisonville','Indianapolis','IN','46240','2002-08-22'); INSERT INTO Phone VALUES (15,'Eric','Gonday','317-555-0500', '9466 Pike Plaza','Greenfield','IN','46533','2002-08-22'); INSERT INTO Phone VALUES (16,'Douglas','Poser','317-555-0123', '55 Tower Pl','Noblesville','IN','46234','2002-08-24'); INSERT INTO Phone VALUES (17,'John','Palmer','317-555-4444', '12433 N Cumberland','Fishers','IN','46038','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" commands would do just as well but tends to clutter the information between the quotes. Also note the use of the entity "&amp;" in the URLs instead of a straight ampersand ("&"). This is necessary to keep WML from assuming that the ampersand is the beginning of an entity name. Menu The menu function displays a simple select list, allowing the user to choose what function he or she wants to access: 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; The code is straightforward, defining a simple <select> list. Each <option> in the list calls the current script, passing the appropriate value of $cmd. Note that we need to handle the case when $cmd is empty, which it will be when the script is first called. In the body, near the beginning of the script, we add the following line: if (empty($cmd)) { $cmd = "Menu"; } 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: // Construct appropriate query $query = "select * from Phone"; if (!empty($search)) { $query = $query." where FirstName like \"%".$search."%\" or"; $query = $query." LastName like \"%".$search."%\""; } 16: 17: // Get first/next five records $query = $query." order by LastName limit ".$idx.",5"; 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: // Display appropriate full/search heading if (empty($search)) { echo "<p mode=\"nowrap\"><b>Phone Book</b>".$lf; } else { echo "<p mode=\"nowrap\"><b>Search Results</b>".$lf; } 31: 32: // Set up Select list (list of five records) echo "<select name=\"View\" title=\"View:\">".$lf; 33: 34: // Go through results from Query, listing each as a CHOICE entry while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) { 35: 36: 37: 38: $recordid = $line[Id]; $Name = $line[LastName] . ", " . $line[FirstName]; $Number = $line[Phone]; $Prompt = $Name . " (" . $Number . ")"; 39: // Build URL for option, include DB index and search 40: $option = "<option onpick=\""; 41: $option = $option."?cmd=Display&amp;id=".$recordid."&amp"; 42: $option = $option.";idx=".$recordid."&amp;search=".$search."\">"; 43: $option = $option.$Prompt."</option>".$lf; 44: echo $option.$lf; 45: } 46: 47: 48: // If there are more records to display, set up paging // else mark end of list (to keep Home as same option) if ($total_rows >= $next) { 49: 50: // Link to next five echo "<option title=\"Next\" onpick=\"?cmd=List&amp;idx=$next"; 51: 52: 53: 54: // Pass Search criteria if exists if (!empty($search)) { echo "&amp;search=".$search; } 55: 56: echo "\">[Next Records]".$lf."</option>".$lf; } else { 57: echo "<option title=\"End of List\" onpick=\"?cmd=List&amp;idx=$idx"; 58: 59: 60: 61: // Pass Search criteria if exists if (!empty($search)) { echo "&amp;search=".$search; } 62: 63: // Close tags echo "\">[End of List]".$lf."</option>".$lf; 64: } 65: // Add option for Home 66: 67: 68: echo "<option onpick=\"?cmd=Menu\" title=\"Home\">".$lf; echo "[Back to Home]".$lf; echo "</option>".$lf; 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; echo "<go href=\"?cmd=List&amp;search=\$searchtext\">".$lf; echo "</go>".$lf; echo "</do>".$lf; echo 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; "</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; $Date = date("M j, Y", strtotime($line[LastUpdate])); echo "Record Updated:<br />$Date"; // Close record display card echo $lf."</p></card>".$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. The Entire Script Now that we've defined the various functions of the script, let's tie it all together with a "switch" statement and some additional initialization statements: <?php header("Content-type: text/vnd.wap.wml"); header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // expires in the past header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // Last modified, right now header("Cache-Control: no-cache, must-revalidate"); // Prevent caching, HTTP/1.1 header("Pragma: no-cache"); // Prevent caching, HTTP/1.0 echo "<?xml version=\"1.0\"?>\n"; echo "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"" . " \"http://www.wapforum.org/DTD/wml_1.1.xml\">\n"; echo "<wml>\n"; // Open link to DB $link = mysql_connect("localhost", "webuser", "webby99") or die("Could not connect to database!"); mysql_select_db("customers") or die("Could not select database!"); // Line feed $lf = chr(10); // Make sure that index into DB has value 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."%\""; } // Get first/next five records $query = $query." order by LastName limit ".$idx.",5"; $result = mysql_query($query,$link) or die("Query failed:$query"); // Advance DB index $next = $idx + 5; // Start card echo "<card id=\"Contacts\">\n"; echo "<do type=\"accept\" label=\"View\"> <go href=\"\"/> </do>".$lf; // Display appropriate full/search heading if (empty($search)) { echo "<p mode=\"nowrap\"><b>Phone Book</b>".$lf; } else { echo "<p mode=\"nowrap\"><b>Search Results</b>".$lf; } // Set up Select list (list of five records) echo "<select name=\"View\" title=\"View:\">".$lf; // Go through results from Query, listing each as a CHOICE entry while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) { $recordid = $line[Id]; $Name = $line[LastName] . ", " . $line[FirstName]; $Number = $line[Phone]; $Prompt = $Name . " (" . $Number . ")"; // Build URL for option, include DB index and search $option = "<option onpick=\""; $option = $option."?cmd=Display&amp;id=".$recordid."&amp"; $option = $option.";idx=".$recordid."&amp;search=".$search."\">"; $option = $option.$Prompt."</option>".$lf; echo $option.$lf; } // If there are more records to display, set up paging // else mark end of list (to keep Home as same option) if ($total_rows >= $next) { // Link to next five echo "<option title=\"Next\" onpick=\"?cmd=List&amp;idx=$next"; // Pass Search criteria if exists if (!empty($search)) { echo "&amp;search=".$search; } echo "\">[Next Records]".$lf."</option>".$lf; } else { echo "<option title=\"End of List\" onpick=\"?cmd=List&amp;idx=$idx"; // Pass Search criteria if exists if (!empty($search)) { echo "&amp;search=".$search; } // Close tags echo "\">[End of List]".$lf."</option>".$lf; } // Add option for Home echo "<option onpick=\"?cmd=Menu\" title=\"Home\">".$lf; echo "[Back to Home]".$lf; echo "</option>".$lf; // Close select echo "</select>".$lf; // Close card echo "</p>".$lf."</card>".$lf; break; case "Search"; echo echo echo echo echo "<card id=\"Search\">\n"; "<do type=\"accept\" label=\"Go\">".$lf; "<go href=\"?cmd=List&amp;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; $Date = date("M j, Y", strtotime($line[LastUpdate])); echo "Record Updated:<br />$Date"; // Close record display card echo $lf."</p></card>".$lf; } break; } echo $lf."</wml>".$lf; mysql_close($link); ?> Note that we pass a handful of headers at the beginning of the script to inhibit caching. Since our database is frequently updated and correct/up-to-date information in the field is valuable, we do not want the device to display cached information instead of recently updated information. However, generally speaking, inhibiting the cache is a bad idea and should be done sparingly, if at all. The Script in Action Now let's see the script in action. The following figures demonstrate each function: Note: All images 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.) Figure 1 - The menu. Figure 2 - The list. Notice the [Next Records] option. Figure 3 - The end of the list. Notice the [End of List] option. Figure 4 - The search form. 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." Figure 6 - A record in the display card. Room for Improvement This script has plenty of room for improvement, including the following items: • Applying the cache inhibitor headings only to cards that could cause problems (such as Display), instead of globally. • Optimizing the output to avoiding duplicating code (such as the addition of search criteria to the URL(s)). • Adding more prompts for the user through card titles, etc. • Optimizing and standardizing variable naming and usage. • Providing means for the user to edit records. Although it can be tedious to enter data on most mobile devices, simple corrections or notes would be welcome. A "last called" field could also be entered automatically each time a contact is called. This script represents only a small portion of what can be done with PHP and a database such as MySQL. This example could be expanded to offer group calendaring, scheduling, order placement, stock checking, etc. As long as you keep the target audience and the respective design goals in mind, the sky's the limit. Delivering HTML To a WML Device This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers techniques to use when delivering standard HTML to WML-compatible devices. Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. Delivering Converted HTML There may be several reasons why you may need to deliver standard HTML markup text to a WML-compatible device. You may have data stored in a database that is typically displayed in a standard browser; legacy data or pages that resist conversion; or cross-platform text that needs to be primarily available for a standard HTML browser, but would be useful if delivered to WML clients. For example, I'm the administrator for a movie news Web site. The articles for the site are marked up using standard HTML, stored in a SQL database, and delivered to the clients using PHP pages. The bitesized articles also make for great wireless content_something that can be browsed while in an airport or during other downtime_so I decided to make this content available in WML. Unfortunately, I quickly found out how incompatible even minor HTML tags are with WML, creating the need for some simple conversion procedures. Although not perfect, those procedures are used as the basis for this article. Tip: The quick-and-dirty methods described in this article are handy as a temporary or short-term measure. If you intend to support a particular platform long-term, I recommend creating custom code for that platform. Standard HTML vs. WML Standard HTML documents don't work well on WML-capable devices. Even if the wireless services offer translation services through their gateways, standard HTML seldom displays as the developer or user would like. Limited Tags WML supports a very limited subset of HTML tags. Among those supported are the following tags: • Character formatting â—¦ <B> - Bold â—¦ <I> - Italic â—¦ <U> - Underline â—¦ <BIG> - Big text â—¦ <SMALL> - Small text â—¦ <STRONG> - Strong (visually emphasized) text â—¦ <P> - Paragraph • Table tags â—¦ <TABLE> â—¦ <TR> - Table row â—¦ <TD> - Table column/cell Several tags nearly alike between the two languages, but their formatting and/or parameters are different enough to cause problems. For example, the line break tag is simply <br> in HTML, but <br /> in WML. Also, tags such as the table tags support many more options and parameters in HTML than in WML, and rarely allow HTML tables to display properly in WML. Device Display Limitations Standard HTML documents are generally designed for large displays, such as 800 x 600 resolution CRTs connected to a PC, not a 240 x 320 LCD on a PDA (or smaller, if a cell phone). Even devices that run HTML-compliant browsers (such as IE in Windows CE devices) have problems with the majority of today's Web sites. The almost unrecognizable internet.com home page, displayed in IE on a Pocket PC (Windows CE). Tip: To gauge roughly how a page will look on a smaller device, shrink your standard PC browser window down to that size. Most mobile devices don't support the vast array of text formatting available toPC browsers. For example, earlier versions of certain mobile browsers don't support underlining; others don't support italic or bold text. Tables are especially problematic due to their width. Device memory is also a problem. Most mobile browsers only support pages (decks) a few kilobytes in size, requiring the content to be broken down into bite-sized chunks and displayed across several cards, if not several decks. Finally, most modern PC-based browsers (IE, Mozilla, Netscape, and so on) have built-in logic to handle incomplete or misused tags. For example, most PC browsers are forgiving of HTML documents that fail to close a major element such as a table or the body of the document. Most mobile browsers are far less forgiving, requiring very strict use of tags. Device Input Limitations Interactive Web pages present even more challenges to the mobile user. Anyone who has needed to tap/write out even a short note on a PDA can appreciate the need to keep interfaces simple. Those who have tried to compose more than just a few characters on a standard cell phone keypad can appreciate this even more strongly. The simplest Web interface is the form, whose structure is considerably different in WML. Simply converting the structure and tags isn't sufficient; you also have to consider how it will affect the end user on his or her individual platform. For example, choosing the correct state code from a drop-down list is easy on a standard browser. However, drop-down lists translate to select lists in WML, necessitating a list of 50 entries that the user must scrolled through (usually 9 items per page) to select the proper code. When Is Converting Worth the Effort? Given the discussion above, there are a few HTML-to-WML conversions that are more problematic than they are worth: • Tables Unless you know that every table in the document is extremely narrow and contains no fancy formatting/parameters, you should simply remove the tags. • Graphics Some gateways will convert graphic files into the prerequisite WBMP format. However, most will simply refuse to display the standard JPG/GIF/PNG Web formats. Unless you have the appropriate graphics available in the WBMP format, remove the graphics. • Code HTML pages that rely on Java, JavaScript, or some other scripting language generally will not be compatible with mobile devices, especially those compatible only with WML. Devices using IE (such as CE-equipped PDAs) will fare much better, but you can't rely on that. In short, only textual pages are worth the time to convert. More complex pages should be redesigned for each individual platform you want to support. Keep in mind that straight WML does not have the facility to convert HTML--you must use a CGI or PHP script to deliver the content instead. Note: See the two previous articles on how to integrate PHP into your WML delivery. 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); Note: PHP functions that support regular expressions can be more versatile and can do more work per statement if constructed correctly. I prefer to use individual statements for later flexibility and clearer code. Miscellaneous Cleanup Two more items need to be cleaned up to display correctly in WML: ampersands ("&") and dollar signs ("$"). An ampersand must be converted to an entity ("&amp;"), and a dollar sign must be doubled ("$$"). Again, in PHP you can use the str_replace function: $wml = str_replace("&","&amp;",$wml); $wml = str_replace("$","$$",$wml); Note: An abundance of special characters can find their way into otherwise mundane HTML code. For example, when text is cut-andpasted from a word processing document into HTML documents, single and double quotes usually appear as extended ASCII characters, and must be converted to the appropriate plain text characters or HTML entities. Only direct experience and experimentation with your specific documents can determine what problems you may have and need to work around. Interactive Fun and Games with WAP and WML This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers creating an interactive game for deployment on mobile WML devices. Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. Uses for Mobile Devices Mobile devices are the most useful when they are connected to data sources and have the ability to deliver various data whenever needed. However, mobile devices are also very useful for entertainment purposes-I've spent many hours in airports with only my PDA and phone for company. Although WML doesn't lend itself to a complex gaming experience, it is fairly easy to create interactive entertainment. In this article I will lead you through the steps to create a rudimentary "hangman" game. 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 graphics-capable device. The figure will be made of 6 segments, giving the player 6 letter selections. • Several cards will be used, one for each function. That will help segregate the functions and keep the display lean. • The words must be random and taken from a decent-sized list to keep the player engaged. • To simplify coding, all words will be in lowercase and the player's input will be forced to lowercase. Coding the Basics This project will utilize WML for the input and output and WMLScript for the behind-the-scenes processing. Because debugging tools for WMLScript are limited, we will build this project in small stages, adding features only after the current feature set is solid. To start, we will create a simple deck of two cards. The first card will call a WMLScript function to initialize the game's variables. The second card will then be displayed, showing the word as blanks and as plain text (for debugging purposes). Our skeletal code looks like this: 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 <!-- When this card is entered, call the init routine --> 7 <onevent type="onenterforward"> 8 <go href="hangman.wmls#init()"/> 9 </onevent> 10 <p> 11 <!-- Tell the user we are initializing. When the 12 init function is finished it will call card2 --> 13 Initializing... 14 </p> 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 the new $blank variable. 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.) The new code allows for input and displays debugging output. Note that "a" was guessed in the previous round, revealing the 3 a's in "banana." Because the $guess variable is persistent, the value sticks in the input field. We will have to fix that eventually. Adding the Scoring (Hangman) As Figure 8.1 shows, we are quickly running out of screen real estate for our game. Since we are using text to depict our hanging man, we will need 4-6 more lines to display him - and we obviously do not have enough lines. Time for an interface change. Instead of putting the input on the same page as the status, we will move it to a "guess" card. It means more work for the user (he or she must select a "guess" button to display the input), but cleans up our interface. For the hanging man, we will use the following ASCII representation: 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> &nbsp;|<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/> &nbsp;$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/> &nbsp;$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"; // Get current values var word = WMLBrowser.getVar("word"); var hang = WMLBrowser.getVar("hang"); var blank = WMLBrowser.getVar("blank"); var guess = WMLBrowser.getVar("guess"); // // // // // Walk one character at a time through word If guess = character, reveal character If guess != character, keep current value (revealed character or blank) Also, set "correct" if at least one char found for (x = 0; x <= String.length(word); x++ ) { if ( String.subString(word,x,1) == guess ) { temp = temp + guess; correct = true; } else { temp = temp + String.subString(blank,x,1); } } // If letter wasn't found, add one to hanging counter if (! correct) { hang++; } // Build our hanging man if (hang > 0) { for (x = 1; x <= hang; x++ ) { man = man + String.elementAt(manpieces,x,","); } } // Blank the guess so <input> is blank guess = ""; // Pass current values to browser WMLBrowser.setVar("blank",temp); WMLBrowser.setVar("guess",guess); WMLBrowser.setVar("hang",hang); WMLBrowser.setVar("man",man); // Determine whether player has won (no more // "*" in blank), has lost (6 pieces of man // displayed, or keep playing (else). if ( String.find(temp,"*") == -1 ) { WMLBrowser.go("hangman.wml#win"); } else { if (hang >= 6) { WMLBrowser.go("hangman.wml#hung"); } else { WMLBrowser.go("hangman.wml#status"); } } } The starting screen of the game, complete with hangman's noose. Pressing the Accept key brings up the guessing card. As the player progresses, correct guesses are shown by revealing characters in the word; incorrect guesses add to the hanging man. 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.) If the player guesses six incorrect letters, the game ends. Guessing all characters in the word, conversely, shows a "win" screen. Note the "Restart" function mapped to Accept. 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. What Do You Want from WML? I'm interested in hearing what you need/want to do with WML. I'll use some of the more challenging or common ideas in upcoming articles. Send your ideas to the address below. Building WML Gadgets: World Time Clock Review As mentioned in the last few articles, it is possible to add value to a mobile device by creating a small but ultimately useful application. In designing such an application, remember that the user will need to be online to use it, so the application's utility needs to be weighed against the potential cost of use. Note: Most mobile service plans offer a base amount of online time dedicated to "Web" use. So the user generally isn't paying more for the occasional gadget use. World Time Application This article describes how to create another useful small application: a world time clock. This clock tells the time in prominent time zones and areas around the world. If you need to make a call to Sydney, Australia, for example, it would be nice to know if you are in danger of waking someone up, or are calling during the lunch hour. Our world time clock should accomplish the following: • Allow the user to search for a given time zone or area • Display the accurate time for the zone(s) found • Be easy to use, but complete enough to be useful More Help from CGI We could utilize only mobile technologies (WML and WMLScript) to accomplish our goals. However, we'd have to do the following: • Find a comprehensive time zone database • Parse the database into an array in WMLScript • Do time and date calculations to determine the time in various zones from the resulting data At first, this approach doesn't seem too bad. There are a finite number of time zones worldwide. However, you also have to take daylight saving time into account. For example, central Indiana (which includes Indianapolis) does not observe daylight savings. This means that Indiana, geographically in the Central time zone, seemingly bounces between Central and Eastern time zones. (In reality, central Indiana stays on Eastern Standard Time, and the zones around it shift.) A comprehensive open source database exists that can be used for this purpose. The database, often referred to as the tz or zoneinfo database, is packaged with most implementation of the GNU C Library, primarily Linux installations. Several tools are available for reading the data from the database, including a Perl module, Time::Timezone (available from CPAN, www.cpan.org). Note: As with previous articles, teaching Perl is out of the scope of this series. There are numerous sources on the Internet for learning Perl, including the tutorial at http://wdvl.internet.com/Authoring/Languages/Perl/PerlfortheWeb/toc. html. Starting the Script -- Reading Time Zones and Telling Time To start the script, we will simply output the time zones and the current time for each. The simple script shown below accomplishes this objective: Listing: tztest.pl #!/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 use Time::ZoneInfo ':all'; use Date::Calc ':all'; my $zones = Time::ZoneInfo->new(); foreach my $zone ($zones->zones) { $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"; } Sample tztest.pl output: ... 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. Creating WML Cards Using methods described in previous articles, it's relatively easy to output compliant WML for mobile devices. We will need another Perl module, CGI, for a couple of purposes: • Parsing arguments passed to the script • Supplying the mobile browser with an appropriate WML header Note: After my calendar gadget article was posted, a reader wrote to chastise me for writing my own argument-parsing routine. As I stated in the article, the fact that the script was passing itself cleanly formatted parameters was justification for simple argument parsing. However, one point I missed (brought up by the reader) was that the code might go on to be incorporated in other scripts where the sterility of the parameters could not be guaranteed. To help promote solid coding, I've pledged to use the CGI method "param" from now on. To output a WML card, we use the following code: # 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 This sets up the beginnings of a generic card. Subsequent PRINT statements can supply <do> and/or <p> tags accordingly. Let's close up the card: # Print closing tags print <<ENDFOOTER; </p> </card> </wml> ENDFOOTER Now all that's left is more Perl logic to control the flow and the resulting output. The Final Script Here's a listing of our final script: Listing: timezone.pl #!/usr/bin/perl use Time::ZoneInfo ':all'; use Date::Calc ':all'; use CGI qw(:standard); my $findzone = param('findzone'); # 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 # Do we have a time zone to look up? 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) { } if (((index uc($zone), uc($findzone)) != -1) || ( "uc($findzone)" eq "ALL" )) { # Display each match, or all if "ALL" was entered print $zone." :<br/>"; $ENV{TZ} = $zone; ($year,$month,$day, $hour,$min,$sec, $doy,$dow,$dst) = System_Clock(); print $year."-".$month."-".$day." "; print $hour.":".$min.":".$sec."<br/>\n"; $matches++; } # If no time zone matches, tell user if ( $matches eq 0 ) { print "No time zone match for:<br/> \n"; print $findzone."<br/><br/>"; } } # Print closing tags print <<ENDFOOTER; </p> </card> </wml> ENDFOOTER # End of script The most notable addition is the IF statement to control which card is displayed. When the script is first executed (without a parameter), the input card is displayed to allow the user to input search text. The script is then called again with the name/value pair "findzone" and the zones are searched for a match. We've also added logic to tell whether any results were returned. If no results were displayed, the code tells the user ("No time zone match for. . . "). Note: Pay particular attention to the escaped dollar sign ($) in the <go> tag. Without the escape backslash (\), Perl would interpret "$findzone" as one of its local variables. Since the Perl variable "$findzone" is empty when the script is first called, the resulting <go> tag would incorrectly be sent to the browser as follows: <go href="timezone.pl?findzone="/> Room for Improvement As with previous projects, several things could be added to improve our world time clock: • A better format for the time output. Right now values under 10 are output as a single digit. For example, nine o'clock AM is displayed as: "9:0." Using a format mask or some simple logic we could pad the time accordingly (e.g., "09:00"). • Break long listings into multiple pages/cards. For example, the "ALL" timezone listing ends up weighing in at just under 5K. That's five times the suggested 1K card data limit. Using some simple logic, the Perl script could display the data in a sequence of cards, each containing 7-12 records. • Add a control on the results card(s) to return to the search card. • Find/create a more comprehensive time zone database that includes named zones such as EST, CST, etc. (A comprehensive database can be compiled by downloading the source files found at ftp://elsie.nci.nih.gov/pub.) Building WML Gadgets: Phone Message Application This series of articles describes how to provide Web content to mobile devices through WML (Wireless Markup Language). This article covers creating an application to aid the user of a mobile phone. Note: These articles cover WML and WMLScript version 1.1, which are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. 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 same "extending functionality" vein. 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: FIGURE 1 - Our application's design. The operator uses a Web form to send the data to a CGI script that stores the data in a database. The same script is used by a mobile user (recipient) to access that data. We'll use Perl for the CGI script, for the same reasons we've used it previously: It's available for most platforms and extensible enough to perform almost any task necessary. Note: As with previous articles, teaching Perl is out of the scope of this series. There are numerous sources on the Internet for learning Perl, including the tutorial at http://wdvl.internet.com/Authoring/Languages/Perl/PerlfortheWeb/toc. html. Coding the Application Let's break down the individual processes and then code for each. 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 format will work well for our simple application. The database will contain the following fields: • Date and time the message was taken • Caller's name • Caller's message • Caller's phone number We'll use a double vertical bar for our delimiter. We could use a more standard delimiter, such as a comma, but we need something that wouldn't end up in the middle of the message field. In short, our database records will resemble the following: <data and time>||<caller's name>||<caller's message>||<caller's number> HTML Form for Data Entry We'll use a simple HTML form for entering the data: Listing: msgform.html <html> <body> <form name="msgform" method="post" action="phonemsg.pl"> <center>Telephone Message</center><br> <table> <tr><td> <input type="hidden" name="cmd" value="save"> From:</td> <td><input type="text" name="from" size="100" maxlength="100"> </td></tr> <tr><td> Message:</td> <td><textarea name="message" cols="100" rows="5" wrap="virtual"> </textarea> </td></tr> <tr><td>Callback #:</td> <td><input type="text" name="number" size="12" maxlength="12"> </td></tr> <tr><td colspan="2"> <center><input type="submit" value="Submit"></center> </td></tr> </table> </form> </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. The CGI Script Our CGI script will be one multipurpose script, performing the following functions: • Saving the data (caller info) • Listing the caller record(s) • Displaying a selected record's details • Automatically dialing the caller's number • Optionally deleting the record and then calling the number The script could also display the input form. However, for maximum portability, we'll use a simple HTML file. With this method, our form can easily be included in almost any Web page template, simply by applying a style sheet or by cutting-and-pasting the "guts" of the form into another page. Saving the Data The following code fragment saves the data entered into the form: Listing: phonemsg.pl - Save data fragment # Start response page print header; print "<html><head>"; print "<META HTTP-EQUIV=\"Refresh\" CONTENT=\"2; URL='msgform.html'\">"; print "</head><body>Please wait...<p>"; # Grab the parameters $from = param('from'); $message = param('message'); $callback = param('number'); # Remove any vertical bars $message =~tr/|/ /; # 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! </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 # (nextitem + 4 or end of file) if ($nextitem + 4 <= @lines) { $lastitem = $nextitem + 4; } else { $lastitem = @lines - 1; } # Print first item through last item for ($i = $nextitem; $i <= $lastitem; $i++) { ($datetime,$from,$message,$callback) = split /\|\|/,$lines[$i]; print "<option onpick='phonemsg.pl?cmd=display&rec=$i'>"; print $from ." (". $datetime .") \n"; print "</option> \n"; } # Set nextitem for NEXT function $nextitem = $lastitem + 1; # Display NEXT option print "<option onpick='phonemsg.pl?cmd=list&nextitem=$nextitem'>"; print "Next</option> \n"; print "</select> \n</p> \n"; print "</card> \n</wml> \n"; 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'); # Print start 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 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>"; 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") { # Start response page print header; print "<html><head>"; print "<META HTTP-EQUIV=\"Refresh\" CONTENT=\"2; URL='msgform.html'\">"; print "</head><body>Please wait...<p>"; # Grab the parameters $from = param('from'); $message = param('message'); $callback = param('number'); # Remove any vertical bars $message =~tr/|/ /; # 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! </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>"; } # # Call from mobile device to list calls if ("$cmd" eq "list") { #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 # (nextitem + 4 or end of file) if ($nextitem + 4 <= @lines) { $lastitem = $nextitem + 4; } else { $lastitem = @lines - 1; } # Print first item through last item for ($i = $nextitem; $i <= $lastitem; $i++) { ($datetime,$from,$message,$callback) = split /\|\|/,$lines[$i]; print "<option onpick='phonemsg.pl?cmd=display&rec=$i'>"; print $from ." (". $datetime .") \n"; print "</option> \n"; } # Set nextitem for NEXT function $nextitem = $lastitem + 1; # Display NEXT option print "<option onpick='phonemsg.pl?cmd=list&nextitem=$nextitem'>"; print "Next</option> \n"; print "</select> \n</p> \n"; print "</card> \n</wml> \n"; } # # Call from mobile device to show call detail if ("$cmd" eq "display") { # 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 } # # Call from mobile device to dial number and del record if ("$cmd" eq "callNdel" ) { # 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'); # Print start 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 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- Using this data, our application resembles the following graphics on a mobile device: FIGURE 2 - The laundry list of messages. FIGURE 3 - A selected message is displayed. 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.) Room for Improvement This application makes a nice, general phone message system. However, given time and incentive, the following improvements could be made: • The code could be streamlined. Because it was written in sections for this article, the code is not as svelte as it could be_in multiple places, code is duplicated that could be placed in commonly accessed functions/subroutines. Also, the code breaks a few "good Perl coding" rules (non-local variables, loose variable naming, etc.); that problem should be rectified. • There's no value checking in the HTML form and it is only set up to accept domestic numbers (12 characters, area code, prefix, suffix, and two dashes). • A real database structure could be used for the data, alleviating the need for stringent file locking and enabling true random access. • Another option could be added to enable the user to back up through the list of messages (we already allow forward access via the Next link). • A search feature could be added to find particular messages or to display messages in a specified timeframe. • Multiple users could be added by specifying the person taking the message (operator) and the person for whom the message is designated (recipient). Then multiple operators could take messages for multiple recipients. This would also necessitate a login or other authentication process for the mobile user (identifying himself/herself as the intended recipient), unless multiple users return calls from "the pool." • A status field could be added so the records could be tracked. Instead of the record simply existing ("need to call") or being deleted ("called"), a message could be flagged for a variety of purposes, including archiving. Tracking Users Using WML This article describes how to provide Web content to mobile devices through WML (Wireless Markup Language). More specifically, this article covers how to track users; that is, how to recognize a repeat visitor to your site. WML and WMLScript version 1.1 are supported by the majority of mobile devices in use today. The articles assume a working knowledge of HTML and general Web technologies, and further assume that you have read the previous article(s) in this series. The Value of Recognizing Users There are a variety of reasons to implement a recognition system that acknowledges that a user that has previously visited your site. The most useful of these reasons is to remember user preferences. For example, suppose that you offer a service of finding particular restaurants near a user. Each user may prefer a certain type of food, environment, etc., and knowing where the user is located is important so that you can find restaurants in that vicinity. Of course, users don't want to input all their preferences every time they visit your site—it's better to save most of the settings, recognize users when they return, and recall their settings. How To Recognize Users When WML was first implemented, the code could retrieve the user's cell phone number from the device; this phone number could act as a unique identifier. Unfortunately, this ability was recognized as an invasion of privacy, and the feature was discontinued. Now there is no way to retrieve a unique identifier from the user's device. What options are left? You could have the user input his or her telephone number on each visit, assign the visitor a login name, or have the user enter an identifier during each visit. Better yet, why not store the identifier on the user's device for recall each time he or she visits your site? Cookies: The Good, the Bad, and the Ugly The term cookie refers to the HTTP technology that allows a site to store data on a user's machine. When cookies were first used, they were fairly innocuous, intended primarily for storing user preferences. 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. Figure 1 - Cookie data is passed back and forth between the server and client via the HTTP stream, but the data is actually stored on the client side. The information stored in the cookie can be just about any type of data: string, date, an integer, or a real number. Most sites choose to encode cookie data into a lengthy string that can be decoded and parsed by the site code. Note: Windows users can examine the cookies that have been stored on the local machine. Look in your local settings directory(ies) for "cookies" files. Windows XP users can find the cookies in the following directory: C:\Documents and Settings\{Username}\Cookies In addition to data, cookies are stored with a time to live (TTL), specifying how long the cookie should be stored before being discarded. The cookie also indicates the scope for which it should be used—that is, what directory(ies) on the server are valid for that cookie. This allows a site to store multiple cookies with the same name, but different scopes. For example, a site with several sections could store preferences for each section in a cookie named "prefs." Using Cookies with WML Because WML doesn't have any built-in cookie functions, you have to use other technologies to store and retrieve cookies. This article shows how to use Perl, which has robust cookie-handling abilities. We'll use the HTTP header Set-Cookie to set cookies in the examples. Although WML has a <head> tag, don't confuse the HTTP header with the WML card head—they're different animals. Cookies must be set before the end of the HTTP header; the WML <head> comes after the HTTP header has been sent, and therefore it cannot be used to set cookies. You can use almost any language that supports cookie functions to accomplish the goals in this article. For example, PHP's header() function can be used to set cookies. Even more control can be accomplished with PHP's setcookie() function. Several variable structures exist in PHP to read cookies, including $HTTP_COOKIE_VARS and $_COOKIE arrays. Handling Cookies with Perl There are many options for handling cookies in Perl, including simple HTTP methods and even dedicated Perl libraries such as cookie-lib.pl. We'll use the simple HTTP methods for this article. For more robust cookie management, the reader is encouraged to check out other cookie-handling methods, such as cookie-lib.pl and HTTP::Cookies. The former is available online at The CGI Resource Index (http://cgi.resourceindex.com/Programs_and_Scripts/Perl/Cookies/); the latter is from CPAN (http://search.cpan.org/author/RSE/lcwa1.0.0/lib/lwp/lib/HTTP/Cookies.pm). Setting a Cookie with Perl As discussed earlier, we can use the HTTP header Set-Cookie to set cookies. In its simplest usage, this header takes the following form: Set-Cookie: <name of cookie>=<value of cookie> For example, a real header might be as follows: Set-Cookie: name=Steve When this header is passed to a browser, it sets the cookie "name" equal to "Steve." Because the header doesn't include a time to live (TTL), the cookie is only valid for the current session. When the browser is closed, the cookie expires and is deleted. To include a TTL, you add the parameter expires as shown in the following example: Set-Cookie: name=Steve; expires=Monday, 24-Mar-03 23:59:59 GMT 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; Note: The Date::Calc module has been covered in several previous articles. The module is available from CPAN, at http://search.cpan.org/author/STBEY/Date-Calc-5.3/Calc.pod. Reading Cookies with Perl 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" An extended example, displaying the cookie value in WML, is shown below: #!/usr/bin/perl # Break cookies into name/value pairs # and store into cookie array @nvpairs=split(/[,;] */, $ENV{'HTTP_COOKIE'}); foreach $pair (@nvpairs) { ($name, $value) = split(/=/, $pair); $cookie{$name} = $value; } # Define WML deck $deck = ' <wml> <card> <p> Cookie (name) = ' .$cookie{'name'}.' </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; If the cookie was still set, this code would display the following: Cookie (name) = Steve Deleting Cookies To remove a cookie, you simply set the expiration of the cookie to a 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"; Note the use of a minus sign in the Add_Delta_Days function to subtract a year from today. We could just as easily subtract only one day. Tying It All Together Now that we can set and retrieve cookies, what exactly do we do with them? As stated earlier, we could store a variety of information in cookies for use in helping the user interface when the visitor returns to our site. In the earlier restaurant example, for instance, you could utilize the following cookies: Name: User's name Phone-Number: Device phone number Zipcode: Target ZIP code for restaurant matches Genre: User's favorite type of food _ ... 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. Click here for larger image Figure 2 - Flowchart of application using described method On entering the site, the user's browser is checked for an ID cookie. If the cookie is found, the user's preferences are retrieved from the server's database and the site is displayed with those preferences. If the cookie is not set, the user is taken to a preference form and queried for his/her preferences. Those preferences are stored in the server's database and the user's ID is stored as a cookie. http://www.developer.com/lang/php/