19. CGI Programming

Introduction

//----------------------------------------------------------------------------------
// URLs have the same form as in Perl

// Invoking dynamic content is done through the same standard urls:
// http://mox.perl.com/cgi-bin/program?name=Johann&born=1685
// http://mox.perl.com/cgi-bin/program

// Groovy has Groovelets and GSP page support built-in. For a full
// web framework, see Grails: http://grails.codehaus.org/
//----------------------------------------------------------------------------------

Writing a CGI Script

//----------------------------------------------------------------------------------
// as a plain groovelet
param = request.getParameter('PARAM_NAME')
println """
<html><head>
<title>Howdy there!</title>
</head>
<body>
<p>
You typed: $param
</p>
</body>
</html>
"""

// as a groovelet using markup builder
import groovy.xml.MarkupBuilder
writer = new StringWriter()
builder = new MarkupBuilder(writer)
builder.html {
    head {
        title 'Howdy there!'
    }
    body {
        p('You typed: ' + request.getParameter('PARAM_NAME'))
    }
}
println writer.toString()

// as a GSP page:
<html><head>
<title>Howdy there!</title>
</head>
<body>
<p>
You typed: ${request.getParameter('PARAM_NAME')}
</p>
</body>
</html>

// Request parameters are often encoded by the browser before
// sending to the server and usually can be printed out as is.
// If you need to convert, use commons lang StringEscapeUtils#escapeHtml()
// and StringEscapeUtils#unescapeHtml().

// Getting parameters:
who = request.getParameter('Name')
phone = request.getParameter('Number')
picks = request.getParameterValues('Choices') // String array or null

// Changing headers:
response.setContentType('text/html;charset=UTF-8')
response.setContentType('text/plain')
response.setContentType('text/plain')
response.setHeader('Cache-control', 'no-cache')
response.setDateHeader('Expires', System.currentTimeMillis() + 3*24*60*60*1000)
//----------------------------------------------------------------------------------

Redirecting Error Messages

//----------------------------------------------------------------------------------
// The Java Servlet API has a special log() method for writing to the
// web server log.

// To send errors to custom HTML pages, update the web.xml deployment
// descriptor to include one or more <error-page> elements, e.g.:
<error-page>
    <error-code>404</error-code>
    <location>/404.html</location>
</error-page>
<error-page>
    <exception-type>java.lang.NullPointerException</exception-type>
    <location>/NpeError.gsp</location>
</error-page>

// Another trick is to catch an exception within the servlet/gsp code
// and print it out into the HTML as a comment.
//----------------------------------------------------------------------------------

Fixing a 500 Server Error

//----------------------------------------------------------------------------------
// 500 errors could occur if you have compile errors in your script.
// Pre-compile with your IDE or groovyc.

// You can use an expando, mock or map to run your scripts outside
// the web container environment. If you use Jetty as your container
// it has a special servlet tester, for more details:
// http://blogs.webtide.com/gregw/2006/12/16/1166307599250.html
//----------------------------------------------------------------------------------

Writing a Safe CGI Program

//----------------------------------------------------------------------------------
// Web servers should be invoked with an appropriate Java security policy in place.
// This can be used to limit possible actions from hacking attempts.

// Normal practices limit hacking exposure. The JDBC API encourages the use
// of Prepared queries rather than encouraging practices which lead to SQL
// injection. Using system or exec is rarely used either as Java provides
// cross-platform mechanisms for most operating system level functionality.

// Other security measures should be complemented with SSL and authentication.
//----------------------------------------------------------------------------------

Making CGI Scripts Efficient

//----------------------------------------------------------------------------------
// Within the servlet element of your web.xml, there is a <load-on-startup> element.
// Use that on a per servlet basis to pre-load whichever servlets you like.
//----------------------------------------------------------------------------------

Executing Commands Without Shell Escapes

//----------------------------------------------------------------------------------
// As discussed in 19.3 and 19.4:
// Web servers should be invoked with an appropriate Java security policy in place.
// This can be used to limit possible actions from hacking attempts.

// Normal practices limit hacking exposure. The JDBC API encourages the use
// of Prepared queries rather than encouraging practices which lead to SQL
// injection. Using system or exec is rarely used either as Java provides
// cross-platform mechanisms for most operating system level functionality.

// In addition, if authentication is used, security can be locked down at a
// very fine-grained level on a per servlet action or per user (with JAAS) basis.
//----------------------------------------------------------------------------------

Formatting Lists and Tables with HTML Shortcuts

//----------------------------------------------------------------------------------
import groovy.xml.*
// using a builder:
Closure markup = {
    ol {
        ['red','blue','green'].each{ li(it) }
    }
}
println new StreamingMarkupBuilder().bind(markup).toString()
// => <ol><li>red</li><li>blue</li><li>green</li></ol>

names = 'Larry Moe Curly'.split(' ')
markup = {
    ul {
        names.each{ li(type:'disc', it) }
    }
}
println new StreamingMarkupBuilder().bind(markup).toString()
// <ul><li type="disc">Larry</li><li type="disc">Moe</li>
//     <li type="disc">Curly</li></ul>
//-----------------------------

m = { li("alpha") }
println new StreamingMarkupBuilder().bind(m).toString()
//     <li>alpha</li>

m = { ['alpha','omega'].each { li(it) } }
println new StreamingMarkupBuilder().bind(m).toString()
//     <li>alpha</li> <li>omega</li>
//-----------------------------

states = [
    "Wisconsin":  [ "Superior", "Lake Geneva", "Madison" ],
    "Colorado":   [ "Denver", "Fort Collins", "Boulder" ],
    "Texas":      [ "Plano", "Austin", "Fort Stockton" ],
    "California": [ "Sebastopol", "Santa Rosa", "Berkeley" ],
]

writer = new StringWriter()
builder = new MarkupBuilder(writer)
builder.table{
    caption('Cities I Have Known')
    tr{ th('State'); th(colspan:3, 'Cities') }
    states.keySet().sort().each{ state ->
        tr{
            th(state)
            states[state].sort().each{ td(it) }
        }
    }
}
println writer.toString()
// =>
// <table>
//   <caption>Cities I Have Known</caption>
//   <tr>
//     <th>State</th>
//     <th colspan='3'>Cities</th>
//   </tr>
//   <tr>
//     <th>California</th>
//     <td>Berkeley</td>
//     <td>Santa Rosa</td>
//     <td>Sebastopol</td>
//   </tr>
//   <tr>
//     <th>Colorado</th>
//     <td>Boulder</td>
//     <td>Denver</td>
//     <td>Fort Collins</td>
//   </tr>
//   <tr>
//     <th>Texas</th>
//     <td>Austin</td>
//     <td>Fort Stockton</td>
//     <td>Plano</td>
//   </tr>
//   <tr>
//     <th>Wisconsin</th>
//     <td>Lake Geneva</td>
//     <td>Madison</td>
//     <td>Superior</td>
//   </tr>
// </table>

import groovy.sql.Sql
import groovy.xml.MarkupBuilder

dbHandle = null
dbUrl = 'jdbc:hsqldb:...'
def getDb(){
    if (dbHandle) return dbHandle
    def source = new org.hsqldb.jdbc.jdbcDataSource()
    source.database = dbUrl
    source.user = 'sa'
    source.password = ''
    dbHandle = new Sql(source)
    return dbHandle
}

def findByLimit(limit) {
    db.rows "SELECT name,salary FROM employees where salary > $limit"
}

limit = request.getParameter('LIMIT')
writer = new StringWriter()
builder = new MarkupBuilder(writer)
builder.html {
    head { title('Salary Query') }
    h1('Search')
    form{
        p('Enter minimum salary')
        input(type:'text', name:'LIMIT')
        input(type:'submit')
    }
    if (limit) {
        h1('Results')
        table(border:1){
            findByLimit(limit).each{ row ->
                tr{ td(row.name); td(row.salary) }
            }
        }
    }
}
println writer.toString()
//----------------------------------------------------------------------------------

Redirecting to a Different Location

//----------------------------------------------------------------------------------
// The preferred way to redirect to resources within the web application:
dispatcher = request.getRequestDispatcher('hello.gsp')
dispatcher.forward(request, response)
// Old versions of web containers allowed this mechanism to also redirect
// to external resources but this was deemed a potential security risk.

// The suggested way to external sites (less efficient for internal resources):
response.sendRedirect("http://www.perl.com/CPAN/")

// set cookie and forward
oreo = new Cookie('filling', 'vanilla creme')
THREE_MONTHS = 3 * 30 * 24 * 60 * 60
oreo.maxAge = THREE_MONTHS
oreo.domain = '.pleac.sourceforge.net'
whither = 'http://pleac.sourceforge.net/pleac_ruby/cgiprogramming.html'
response.addCookie(oreo)
response.sendRedirect(whither)

// forward based on user agent
dir = 'http://www.science.uva.nl/%7Emes/jargon'
agent = request.getHeader('user-agent')
menu = [
    [/Mac/, 'm/macintrash.html'],
    [/Win(dows )?NT/, 'e/evilandrude.html'],
    [/Win|MSIE|WebTV/, 'm/microslothwindows.html'],
    [/Linux/, 'l/linux.html'],
    [/HP-UX/, 'h/hpsux.html'],
    [/SunOS/, 's/scumos.html'],
]
page = 'a/aportraitofj.randomhacker.html'
menu.each{
    if (agent =~ it[0]) page = it[1]
}
response.sendRedirect("$dir/$page")

// no response output
response.sendError(204, 'No Response')
//----------------------------------------------------------------------------------

Debugging the Raw HTTP Exchange

//----------------------------------------------------------------------------------
// Consider TCPMON or similar: http://ws.apache.org/commons/tcpmon/
//----------------------------------------------------------------------------------

Managing Cookies

//----------------------------------------------------------------------------------
// helper method
import javax.servlet.http.Cookie
import groovy.xml.MarkupBuilder

def getCookieValue(cookies, cookieName, defaultValue) {
    if (cookies) for (i in 0..<cookies.length) {
        if (cookieName == cookies[i].name) return cookies[i].value
    }
    return defaultValue
}

prefValue = getCookieValue(request.cookies, 'preference_name', 'default')
cookie = new Cookie('preference name',"whatever you'd like")
SECONDS_PER_YEAR = 60*60*24*365
cookie.maxAge = SECONDS_PER_YEAR * 2
response.addCookie(cookie)

cookname = 'fav_ice_cream'
favorite = request.getParameter('flavor')
tasty    = getCookieValue(request.cookies, cookname, 'mint')

writer = new StringWriter()
builder = new MarkupBuilder(writer)
builder.html {
    head { title('Ice Cookies') }
    body {
        h1('Hello Ice Cream')
        if (favorite) {
            p("You chose as your favorite flavor '$favorite'.")
            cookie = new Cookie(cookname, favorite)
            ONE_HOUR = 3600 // secs
            cookie.maxAge = ONE_HOUR
            response.addCookie(cookie)
        } else {
            hr()
            form {
                p('Please select a flavor: ')
                input(type:'text', name:'flavor', value:tasty)
            }
            hr()
        }
    }
}
println writer.toString()
//----------------------------------------------------------------------------------

Creating Sticky Widgets

//----------------------------------------------------------------------------------
import groovy.xml.MarkupBuilder
// On Linux systems replace with: "who".execute().text
fakedWhoInput = '''
root tty1 Nov 2 17:57
hermie tty3 Nov 2 18:43
hermie tty4 Nov 1 20:01
sigmund tty2 Nov 2 18:08
'''.trim().split(/\n/)
name = request.getParameter('WHO')
if (!name) name = ''
writer = new StringWriter()
new MarkupBuilder(writer).html{
    head{ title('Query Users') }
    body{
        h1('Search')
        form{
            p('Which User?')
            input(type:'text', name:'WHO', value:name)
            input(type:'submit')
        }
        if (name) {
            h1('Results')
            lines = fakedWhoInput.grep(~/^$name\s.*/)
            if (lines) message = lines.join('\n')
            else message = "$name is not logged in"
            pre(message)
        }
    }
}
println writer.toString()
// if you need to escape special symbols, e.g. '<' or '>' use commons lang StringEscapeUtils
//----------------------------------------------------------------------------------

Writing a Multiscreen CGI Script

//----------------------------------------------------------------------------------
// frameworks typically do this for you, but shown here are the manual steps
// even when doing it manually, you would probably use session variables

// setting a hidden field
input(type:'hidden', value:'bacon')

// setting a value on the submit
input(type:'submit', name:".State", value:'Checkout')

// determining 'mode'
page = request.getParameter('.State')
if (!page) page = 'Default'

// forking with if chain
if (page == "Default") {
    frontPage()
} else if (page == "Checkout") {
    checkout()
} else {
    noSuchPage()
}

// forking with map
states = [
    Default:  this.&frontPage,
    Shirt:    this.&tShirt,
    Sweater:  this.&sweater,
    Checkout: this.&checkout,
    Card:     this.&creditCard,
    Order:    this.&order,
    Cancel:   this.&frontPage,
]

// calling each to allow hidden variable saving
states.each{ key, closure ->
    closure(page == key)
}

// exemplar method
def tShirt(active) {
    def sizes = ['XL', 'L', 'M', 'S']
    def colors = ['Black', 'White']
    if (!active) {
        hidden("size")
        hidden("color")
        return
    }
    p("You want to buy a t-shirt?");
    label("Size:  ");     dropDown("size", sizes)
    label("Color: ");     dropDown("color", colors)
    shopMenu()
}

// kicking off processing
html{
    head{ title('chemiserie store') }
    body {
        if (states[page]) process(page)
        else noSuchPage()
    }
}
//----------------------------------------------------------------------------------

Saving a Form to a File or Mail Pipe

//----------------------------------------------------------------------------------
// get request parameters as map
map = request.parameterMap

// save to file
new File(filename).withOutputStream{ fos ->
    oos = new ObjectOutputStream(fos)
    oos.writeObject(map)
    oos.close()
}

// convert to text
sb = new StringBuffer()
map.each{ k,v -> sb << "$k=$v" }
text = sb.toString()
// to send text via email, see 18.3
//----------------------------------------------------------------------------------

Program: chemiserie

//----------------------------------------------------------------------------------
// you wouldn't normally do it this way, consider a framework like Grails
// even when doing it by hand, you would probably use session variables
import groovy.xml.MarkupBuilder

page = param('.State', 'Default')

states = [
    Default:  this.&frontPage,
    Shirt:    this.&shirt,
    Sweater:  this.&sweater,
    Checkout: this.&checkout,
    Card:     this.&creditCard,
    Order:    this.&order,
    Cancel:   this.&frontPage,
]

writer = new StringWriter()
b = new MarkupBuilder(writer)
b.html{
    head{ title('chemiserie store') }
    body {
        if (states[page]) process(page)
        else noSuchPage()
    }
}
println writer.toString()

def process(page) {
    b.form{
        states.each{ key, closure ->
            closure(page == key)
        }
    }
}

def noSuchPage() {
    b.p('Unknown request')
    reset('Click here to start over')
}

def shopMenu() {
    b.p()
    toPage("Shirt")
    toPage("Sweater")
    toPage("Checkout")
    reset('Empty My Shopping Cart')
}

def frontPage(active) {
    if (!active) return
    b.h1('Hi!')
    b.p('Welcome to our Shirt Shop! Please make your selection from the menu below.')
    shopMenu()
}

def shirt(active) {
    def sizes = ['XL', 'L', 'M', 'S']
    def colors = ['Black', 'White']
    def count = param('shirt_count',0)
    def color = param('shirt_color')
    def size = param('shirt_size')
    // sanity check
    if (count) {
        if (!(color in colors)) color = colors[0]
        if (!(size in sizes)) size = sizes[0]
    }
    if (!active) {
        if (size) hidden("shirt_size", size)
        if (color) hidden("shirt_color", color)
        if (count) hidden("shirt_count", count)
        return
    }
    b.h1 'T-Shirt'
    b.p '''What a shirt! This baby is decked out with all the options.
        It comes with full luxury interior, cotton trim, and a collar
        to make your eyes water! Unit price: $33.00'''
    b.h2 'Options'
    label("How Many?");  textfield("shirt_count")
    label("Size?");      dropDown("shirt_size", sizes)
    label("Color?");     dropDown("shirt_color", colors)
    shopMenu()
}

def sweater(active) {
    def sizes = ['XL', 'L', 'M']
    def colors = ['Chartreuse', 'Puce', 'Lavender']
    def count = param('sweater_count',0)
    def color = param('sweater_color')
    def size = param('sweater_size')
    // sanity check
    if (count) {
        if (!(color in colors)) color = colors[0]
        if (!(size in sizes)) size = sizes[0]
    }
    if (!active) {
        if (size) hidden("sweater_size", size)
        if (color) hidden("sweater_color", color)
        if (count) hidden("sweater_count", count)
        return
    }
    b.h1("Sweater")
    b.p("Nothing implies preppy elegance more than this fine " +
        "sweater. Made by peasant workers from black market silk, " +
        "it slides onto your lean form and cries out ``Take me, " +
        "for I am a god!''. Unit price: \$49.99.")
    b.h2("Options")
    label("How Many?"); textfield("sweater_count")
    label("Size?"); dropDown("sweater_size", sizes)
    label("Color?"); dropDown("sweater_color", colors)
    shopMenu()
}

def checkout(active) {
    if (!active) return
    b.h1("Order Confirmation")
    b.p("You ordered the following:")
    orderText()
    b.p("Is this right? Select 'Card' to pay for the items" +
        "or 'Shirt' or 'Sweater' to continue shopping.")
    toPage("Card")
    toPage("Shirt")
    toPage("Sweater")
}

def creditCard(active) {
    def widgets = 'Name Address1 Address2 City Zip State Phone Card Expiry'.split(' ')
    if (!active) {
        widgets.each{ hidden(it) }
        return
    }
    b.pre{
        label("Name: ");          textfield("Name")
        label("Address: ");       textfield("Address1")
        label(" ");               textfield("Address2")
        label("City: ");          textfield("City")
        label("Zip: ");           textfield("Zip")
        label("State: ");         textfield("State")
        label("Phone: ");         textfield("Phone")
        label("Credit Card #: "); textfield("Card")
        label("Expiry: ");        textfield("Expiry")
    }
    b.p("Click on 'Order' to order the items. Click on 'Cancel' to return shopping.")
    toPage("Order")
    toPage("Cancel")
}

def order(active) {
    if (!active) return
    b.h1("Ordered!")
    b.p("You have ordered the following items:")
    orderText()
    reset('Begin Again')
}

def orderText() {
    def shirts = param('shirt_count')
    def sweaters = param('sweater_count')
    if (shirts) {
        b.p("""You have ordered ${param('shirt_count')}
            shirts of size ${param('shirt_size')}
            and color ${param("shirt_color")}.""")
    }
    if (sweaters) {
        b.p("""You have ordered ${param('sweater_count')}
        sweaters of size ${param('sweater_size')}
        and color ${param('sweater_color')}.""")
    }
    if (!sweaters && !shirts) b.p("Nothing!")
    b.p("For a total cost of ${calcPrice()}")
}

def label(text) { b.span(text) }
def reset(text) { b.a(href:request.requestURI,text) }
def toPage(name) { b.input(type:'submit', name:'.State', value:name) }
def dropDown(name, values) {
    b.select(name:name){
        values.each{
            if (param(name)==it) option(value:it, selected:true, it)
            else option(value:it, it)
        }
    }
    b.br()
}
def hidden(name) {
    if (binding.variables.containsKey(name)) v = binding[name]
    else v = ''
    hidden(name, v)
}
def hidden(name, value) { b.input(type:'hidden', name:name, value:value) }
def textfield(name) { b.input(type:'text', name:name, value:param(name,'')); b.br() }
def param(name) { request.getParameter(name) }
def param(name, defValue) {
    def val = request.getParameter(name)
    if (val) return val else return defValue
}

def calcPrice() {
    def shirts = param('shirt_count', 0).toInteger()
    def sweaters = param('sweater_count', 0).toInteger()
    return (shirts * 33 + sweaters * 49.99).toString()
}
//----------------------------------------------------------------------------------