19. CGI Programming

Introduction

# Introduction
#
# There is no standard cgi/web framework in python,
# this is reason for ranting now and then.
#
# See `PyWebOff <http://pyre.third-bit.com/pyweb/index.html>`__
# which compares CherryPy, Quixote, Twisted, WebWare and Zope
# Karrigell and print stantements. 
#
# Then there is Nevow and Standalone ZPT.

Writing a CGI Script

# Partial implementation of PLEAC Python section 19.1
# Written by Seo Sanghyeon

# Standard CGI module is where PERL shines. Python
# module, cgi, is nothing but a form parser. So it is
# not really fair to compare these two. But I hesitate
# to introduce any non-standard module. After all,
# which one should I choose?

# I would stick to simple print statements. I believe
# the following is close to how these tasks are usually
# done in Python.

#-----------------------------
#!/usr/bin/env python
# hiweb - using FieldStorage class to get at form data

import cgi
form = cgi.FieldStorage()

# get a value from the form
value = form.getvalue("PARAM_NAME")

# print a standard header
print "Content-Type: text/html"
print

# print a document
print "<P>You typed: <TT>%s</TT></P>" % (
    cgi.escape(value),
    )

#-----------------------------
import cgi
form = cgi.FieldStorage()

who = form.getvalue("Name")
phone = form.getvalue("Number")
picks = form.getvalue("Choices")

# if you want to assure `picks' to be a list
picks = form.getlist("Choices")

#-----------------------------
# Not Implemented

# To implement -EXPIRES => '+3d', I need to study about
import cgi
import datetime

time_format = "%a, %d %b %Y %H:%M:%S %Z"
print "Expires: %s" % (
        (datetime.datetime.now()
        + datetime.timedelta(+3)).strftime(time_format)
        )
print "Date: %s" % (datetime.datetime.now().strftime(time_format))
print "Content-Type: text/plain; charset=ISO-8859-1"

#-----------------------------
# NOTES

# CGI::param() is a multi-purpose function. Here I want to
# note which Python functions correspond to it.

# PERL version 5.6.1, CGI.pm version 2.80.
# Python version 2.2.3. cgi.py CVS revision 1.68.

# Assume that `form' is the FieldStorage instance.

# param() with zero argument returns parameter names as
# a list. It is `form.keys()' in Python, following Python's
# usual mapping interface.

# param() with one argument returns the value of the named
# parameter. It is `form.getvalue()', but there are some
# twists:

# 1) A single value is passed.
# No problem.

# 2) Multiple values are passed.
# PERL: in LIST context, you get a list. in SCALAR context,
#       you get the first value from the list.
# Python: `form.getvalue()' returns a list if multiple
#         values are passed, a raw value if a single value
#         is passed. With `form.getlist()', you always
#         get a list. (When a single value is passed, you
#         get a list with one element.) With `form.getfirst()',
#         you always get a value. (When multiple values are
#         passed, you get the first one.)

# 3) Parameter name is given, but no value is passed.
# PERL: returns an empty string, not undef. POD says this
#       feature is new in 2.63, and was introduced to avoid
#       "undefined value" warnings when running with the
#       -w switch.
# Python: tricky. If you want black values to be retained,
#         you should pass a nonzero `keep_blank_values' keyword
#         argument. Default is not to retain blanks. In case
#         values are not retained, see below.

# 4) Even parameter name is never mentioned.
# PERL: returns undef.
# Python: returns None, or whatever you passed as the second
#         argument, or `default` keyword argument. This is
#         consistent with `get()' method of the Python mapping
#         interface.

# param() with more than one argument modifies the already
# set form data. This functionality is not available in Python
# cgi module.

Redirecting Error Messages

# enable() from 'cgitb' module, by default, redirects traceback
# to the browser. It is defined as 'enable(display=True, logdir=None,
# context=5)'.

# equivalent to importing CGI::Carp::fatalsToBrowser.
import cgitb
cgitb.enable()

# to suppress browser output, you should explicitly say so.
import cgitb
cgitb.enable(display=False)

# equivalent to call CGI::Carp::carpout with temporary files.
import cgitb
cgitb.enable(logdir="/var/local/cgi-logs/")

# Python exception, traceback facilities are much richer than PERL's
# die and its friends. You can use your custom exception formatter
# by replacing sys.excepthook. (equivalent to CGI::Carp::set_message.)
# Default formatter is available as traceback.print_exc() in pure
# Python. In fact, what cgitb.enable() does is replacing excepthook
# to cgitb.handler(), which knows how to format exceptions to HTML.

# If this is not enough, (usually this is enough!) Python 2.3 comes
# with a new standard module called 'logging', which is complex, but
# very flexible and entirely customizable.

Fixing a 500 Server Error

#
# download the following standalone program
#!/usr/bin/python
# webwhoami - show web users id
import getpass
print "Content-Type: text/plain\n"
print "Running as %s\n" % getpass.getuser()



# STDOUT/ERR flushing
#
# In contrast to what the perl cookbook says, modpython.org tells
# STDERR is buffered too.

Writing a Safe CGI Program

# @@INCOMPLETE@@
# @@INCOMPLETE@@

Making CGI Scripts Efficient


# use mod_python in the Apache web server.

# Load the module in httpd.conf or apache.conf

LoadModule python_module libexec/mod_python.so

<Directory /some/directory/htdocs/test>
    AddHandler mod_python .py
    PythonHandler mptest
    PythonDebug On
</Directory>

# test.py file in /some/directory/htdocs/test
from mod_python import apache

def handler(req):
    req.write("Hello World!")
    return apache.OK

Executing Commands Without Shell Escapes


import os
os.system("command %s %s" % (input, " ".join(files))) # UNSAFE

# python doc lib cgi-security it says
#
# To be on the safe side, if you must pass a string gotten from a form to a shell
# command, you should make sure the string contains only alphanumeric characters, dashes,
# underscores, and periods.
import re
cmd = "command %s %s" % (input, " ".join(files))
if re.search(r"[^a-zA-Z0-9._\-]", cmd):
    print "rejected"
    sys.exit(1)
os.system(cmd)
trans = string.maketrans(string.ascii_letters+string.digits+"-_.",

# @@INCOMPLETE@@
# @@INCOMPLETE@@

Formatting Lists and Tables with HTML Shortcuts

#-----------------------------
# This uses nevow's (http://nevow.com) stan; there's no standard
# way to generate HTML, though there are many implementations of
# this basic idea.
from nevow import tags as T
print T.ol[T.li['red'], T.li['blue'], T.li['green']]
# <ol><li>red</li><li>blue</li><li>green</li></ol>

names = 'Larry Moe Curly'.split()
print T.ul[ [T.li(type="disc")[name] for name in names] ]
# <ul><li type="disc">Larry</li><li type="disc">Moe</li>
#     <li type="disc">Curly</li></ul>
#-----------------------------
print T.li["alpha"]
#     <li>alpha</li>

print T.li['alpha'], T.li['omega']
#     <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" ],
}

print "<TABLE> <CAPTION>Cities I Have Known</CAPTION>";
print T.tr[T.th('State'), T.th('Cities')]
for k in sorted(states.keys()):
    print T.tr[ [T.th(k)] + [T.td(city) for city in sorted(states[k])] ]
print "</TABLE>";
#-----------------------------
# <TABLE> <CAPTION>Cities I Have Known</CAPTION>
#
#     <TR><TH>State</TH> <TH>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>
#-----------------------------
print T.table[
        [T.caption['Cities I have Known'],
         T.tr[T.th['State'], T.th['Cities']] ] +
        [T.tr[ [T.th(k)] + [T.td(city) for city in sorted(states[k])]]
         for k in sorted(states.keys())]]
#-----------------------------
# salcheck - check for salaries
import MySQLdb
import cgi

form = cgi.FieldStorage()

if 'limit' in form:
    limit = int(form['limit'].value)
else:
    limit = ''

# There's not a good way to start an HTML/XML construct with stan
# without completing it.
print '<html><head><title>Salary Query</title></head><body>'
print T.h1['Search']
print '<form>'
print T.p['Enter minimum salary',
          T.input(type="text", name="limit", value=limit)]
print T.input(type="submit")
print '</form>'

if limit:
    dbconn = MySQLdb.connect(db='somedb', host='server.host.dom',
                             port=3306, user='username',
                             passwd='password')
    cursor = dbconn.cursor()
    cursor.execute("""
    SELECT name, salary FROM employees
    WHERE salary > %s""", (limit,))

    print T.h1["Results"]
    print "<TABLE BORDER=1>"

    for row in cursor.fetchall():
        print T.tr[ [T.td(cell) for cell in row] ]

    print "</TABLE>\n";
    cursor.close()
    dbconn.close()

print '</body></html>'
#-----------------------------

Redirecting to a Different Location

#-----------------------------
url = "http://python.org/pypi"
print "Location: %s\n" % url
raise SystemExit
#-----------------------------
# oreobounce - set a cookie and redirect the browser
import Cookie
import time

c = Cookie.SimpleCookie()
c['filling'] = 'vanilla cr?me'
now = time.time()
future = now + 3*(60*60*24*30) # 3 months
expire_date = time.strftime('%a %d %b %Y %H:%M:%S GMT', future)
c['filling']['expires'] = expire_date
c['filling']['domain'] = '.python.org'

whither  = "http://somewhere.python.org/nonesuch.html"

# Prints the cookie header
print 'Status: 302 Moved Temporarily'
print c
print 'Location:', whither
print

#-----------------------------
#Status: 302 Moved Temporarily
#Set-Cookie: filling=vanilla%20cr%E4me; domain=.perl.com;
#    expires=Tue, 21-Jul-1998 11:58:55 GMT
#Location: http://somewhere.perl.com/nonesuch.html
#-----------------------------
# os_snipe - redirect to a Jargon File entry about current OS
import os, re
dir = 'http://www.wins.uva.nl/%7Emes/jargon'
matches = [
    (r'Mac', 'm/Macintrash.html'),
    (r'Win(dows )?NT', 'e/evilandrude.html'),
    (r'Win|MSIE|WebTV', 'm/MicroslothWindows.html'),
    (r'Linux', 'l/Linux.html'),
    (r'HP-UX', 'h/HP-SUX.html'),
    (r'SunOS', 's/ScumOS.html'),
    (None, 'a/AppendixB.html'),
    ]

for regex, page in matches:
    if not regex: # default
        break
    if re.search(regex, os.environ['HTTP_USER_AGENT']):
        break
print 'Location: %s/%s\n' % (dir, page)
#-----------------------------
# There's no special way to print headers
print 'Status: 204 No response'
print
#-----------------------------
#Status: 204 No response
#-----------------------------

Debugging the Raw HTTP Exchange

# download the following standalone program
#!/usr/bin/python
# dummyhttpd - start a HTTP daemon and print what the client sends

import SocketServer
# or use BaseHTTPServer, SimpleHTTPServer, CGIHTTPServer

def adr_str(adr):
    return "%s:%d" % adr

class RequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        print "client access from %s" % adr_str(self.client_address)
        print self.request.recv(10000)
        self.request.send("Content-Type: text/plain\n"
                          "Server: dymmyhttpd/1.0.0\n"
                          "\n...\n")
        self.request.close()


adr = ('127.0.0.1', 8001)
print "Please contact me at <http://%s>" % adr_str(adr)
server = SocketServer.TCPServer(adr, RequestHandler)
server.serve_forever()
server.server_close()

Managing Cookies


import Cookie
cookies = Cookie.SimpleCookie()
# SimpleCookie is more secure, but does not support all characters.
cookies["preference-name"] = "whatever you'd like" 
print cookies

# download the following standalone program
#!/usr/bin/python
# ic_cookies - sample CGI script that uses a cookie

import cgi
import os
import Cookie
import datetime

cookname = "favorite-ice-cream"  # SimpleCookie does not support blanks
fieldname = "flavor"

cookies = Cookie.SimpleCookie(os.environ.get("HTTP_COOKIE",""))
if cookies.has_key(cookname):
    favorite = cookies[cookname].value
else:
    favorite = "mint"

form = cgi.FieldStorage()
if not form.has_key(fieldname):
    print "Content-Type: text/html"
    print "\n"
    print "<html><body>"
    print "<h1>Hello Ice Cream</h1>"
    print "<form>"
    print 'Please select a flavor: <input type="text" name="%s" value="%s" />' % (
            fieldname, favorite )
    print "</form>"
    print "<hr />"
    print "</body></html>"
else:
    favorite = form[fieldname].value
    cookies[cookname] = favorite
    expire = datetime.datetime.now() + datetime.timedelta(730)
    cookies[cookname]["expires"] = expire.strftime("%a, %d %b %Y %H:00:00 GMT")
    cookies[cookname]["path"] = "/"
    print "Content-Type: text/html"
    print cookies
    print "\n"
    print "<html><body>"
    print "<h1>Hello Ice Cream</h1>"
    print "<p>You chose as your favorite flavor \"%s\"</p>" % favorite
    print "</body></html>"

Creating Sticky Widgets

# @@INCOMPLETE@@
# @@INCOMPLETE@@

Writing a Multiscreen CGI Script

# @@INCOMPLETE@@
# @@INCOMPLETE@@

Saving a Form to a File or Mail Pipe

#-----------------------------
# first open and exclusively lock the file
import os, cgi, fcntl, cPickle
fh = open('/tmp/formlog', 'ab')
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)

form = cgi.FieldStorage()
# This doesn't produce a readable file; we copy the environment so
# that we save a plain dictionary (os.environ is a dictionary-like
# object).
cPickle.dump((form, os.environ.copy()) fh)
fh.close()
#-----------------------------
import cgi, smtplib, sys

form = cgi.FieldStorage()
email = """\
From: %S
To: hisname@hishost.com
Subject: mailed form submission

""" % sys.argv[0]

for key in form:
    values = form[key]
    if not isinstance(values, list):
        value = [values.value]
    else:
        value = [v.value for v in values]
    for item in values:
        email += '\n%s: %s' % (key, value)

server = smtplib.SMTP('localhost')
server.sendmail(sys.argv[0], ['hisname@hishost.com'], email)
server.quit()
#-----------------------------
# @@INCOMPLETE@@ I don't get the point of these:
# param("_timestamp", scalar localtime);
# param("_environs", %ENV);
#-----------------------------
import fcntl, cPickle
fh = open('/tmp/formlog', 'rb')
fcntl.flock(fh.fileno(), fcntl.LOCK_SH)

count = 0
while True:
    try:
        form, environ = cPickle.load(fh)
    except EOFError:
        break
    if environ.get('REMOTE_HOST').endswith('perl.com'):
        continue
    if 'items requested' in form:
        count += int(form['items requested'].value)
print 'Total orders:', count
#-----------------------------

Program: chemiserie

# @@INCOMPLETE@@
# @@INCOMPLETE@@