# cgiApp.py -- Simple framework for generating run-of-the-mill
#    cgi applications.

"""A simple framework for plain-vanilla CGI programs. A cgi program will
subclass cgiApp.cgiApp and call the run() method to execute the CGI.
"""

import cgi, sys, string, signal, os

# CGIQuit is an exception class used to terminate a CGI script
# "prematurely" (not completing the normal method invocation
# sequence).  You must send a response before raising this.

class CGIQuit: pass

# generic format for an HTML reply.
HTMLReply = \
"""Connection: close\r
Content-Type: text/html\r
\r
<html>\r
<head>\r
<title>%(title)s</title>\r
<style type="text/css">
%(stylesheet)s\r
</style>
</head>\r
<body>\r
%(body)s\r
</body>\r
</html>"""

# generic error format. This is used for the default error messages.
# probably not useful in production code.
Errorfmt = """\
<h1> An error occurred in processing your request. </h1>
<dl>
<dt style="Top-margin: 2em"><b> Type of Error:</b></dt>
<dd>%(type)s</dd>
<dt style="Top-margin: 2em"><b> Cause of Error:</b></dt>
<dd>%(value)s</dd>
</dl>"""

# Helper functions for formatting responses.
def send_reply(title, body, stylesheet=""):
    """Send a correctly formatted HTML document having the given
    title and body. The value of style will be used as the document
    style rules. All three parameters should be strings.
    """
    fields = {"title": title, "body": body, "stylesheet": stylesheet}
    sys.stdout.write( HTMLReply % fields )

def send_error(type, value, style=""):
    """Send an error message using the default error format. The type,
    value and attributes are all strings. Type describes what kind of
    error occurred, and value explains the cause.
    """
    fields = {"type": type, "value": value}
    send_reply( "ERROR", Errorfmt % fields, style)

def send_headers():
    """Send the Content-Type header. Useful for constructing an
    abortive reply before raising CGIQuit
    """
    print "Connection: Close\r"
    print "Content-Type: text/html\r"
    print

# SafeStdin is used when reading posted data.
class SafeStdin:
    """Standard input file clone, that redefines f.read()
    to allow signals to be detected during the read.
    """
    def __getattr__(self, name):
        """Any attribute except read is handled by normal stdin.
        """
        return getattr(sys.stdin, name)

    def read(self, length):
        """Always reads stdin one character at a time.
        """
        buff = [""] * length
        fp = sys.stdin
        for i in xrange(length):
            buff[i] = fp.read(1)
        return string.join(buff,'')

class CGIApp:
    """Standard base class for deriving CGI applications. Instantiate
    the class and call the run() method. This initiates the sequence
    of calls: ProcessRequest(), GetTitle(), GetStylesheet(), GetBody()
    which provide the logic of the application. A script can be terminated
    early by raising CGIQuit. Cleanup is performed by one of SuccessCleanup()
    (normal completion) AbortCleanup() (on CGIQuit) or UnhandledError()
    (on any other exception).
    """

    # Amount of time allowed to read posted data. Redfine with an
    #     int value to set a limit.  Most apps should do this.
    DATA_TIMEOUT = None

    # LENGTH_LIMIT on posted data. Redefine with an int value
    #      to limit the amount of input that will be read.
    #      Most apps should do this.
    LENGTH_LIMIT = None

    # REQ_FIELDS is a list of the field names that must be supplied by
    #      the cgi request. May (should) be redefined
    REQ_FIELDS = []

    # OPT_FIELDS is a list of (name, default) pairs. If the name is present
    #    in the form with a non-empty value, its value will be used. Otherwise,
    #    the name will be given the default value. May (should) be redefined.
    OPT_FIELDS = []

    def run(self):
        "Call this method to run the CGI application."
        
        try:
            self.get_form()
            self.ProcessRequest(self.formdict)
            title = self.GetTitle()
            style = self.GetStylesheet()
            body = self.GetBody()
            send_reply(title, body, style)
        except CGIQuit:
            # Early termination
            self.AbortCleanup()
        except:
            # Python exception that was not handled (caught)
            self.UnhandledError()
        else:
            # Normal termination.
            self.SuccessCleanup()

    def get_form(self):
        self.check_length()
        dict = self.formdict = {}
        data = self.get_fieldStorage()
        
        for v in self.REQ_FIELDS:
            if data.has_key(v):
                dict[v] = data.getfirst(v)
            else:
                self.MissingValue(v)
                raise CGIQuit()
            
        for (v, default) in self.OPT_FIELDS:
            dict[v] = data.getfirst(v,default)

    def get_fieldStorage(self):
        timeout = self.DATA_TIMEOUT
        if timeout != None:
            # set a timeout
            signal.signal(signal.SIGALRM, self.AlarmHandler)
            signal.alarm(timeout)
        form = self.form = cgi.FieldStorage(SafeStdin())
        if timeout != None:
            # cancel the timeout
            signal.alarm(0)
        return form
                   
    def check_length(self):
        cll = self.LENGTH_LIMIT
        if cll != None:
            try:
                CL = os.environ["CONTENT_LENGTH"]
            except KeyError:
                return # no content length means "ok"
            else:
                cl = int(CL)
            if cl>cll:
                self.DataTooLarge(cl)
                raise CGIQuit() # abort

    #--------------------------------------------------------------
    # Redefine the following to build your CGI app.
    #  Form field values are accessed from a dictionary self.formdict
    #  which is also sent as a parameter to Process_Request.

    def ProcessRequest(self, formdict):
        """Performs any inital processing the application must do. The
        formdict is a dictionary that contains the form data. Note: this is
        also in self.formdict"""
        pass

    def GetTitle(self):
        """Returns the title of the reponse page"""
        return "Generic CGI APP"

    def GetBody(self):
        """Returns the body of the response page"""
        return "<h1> Hello CGI World </h1>"

    def GetStylesheet(self):
        """Returns a string of CSS rules for the document"""
        return ""

    # Exactly one of the following three methods will be called at
    #     the termination of your CGI. The default behavior for
    #     Unhandled_Error is very useful for debugging. It should
    #     probably be over-ridden in the final production app.

    def AbortCleanup(self):
        """Called at end of processing terminated with CGIQuit.
        Can be used to close files, write logs, etc.
        """
        pass

    def SuccessCleanup(self):
        """Called at end of normal processing. Can be used to close
        files, write logs, etc.
        """
        pass
   
    def UnhandledError(self):
        """Called if processing is ended by a Python exception.
        """
        send_headers()
        print "<html><head><title>ERROR: Unhandled Exception</title></head>"
        print "<body><h1>An Unhandled Exception Occurred</h1>"
        cgi.print_exception()
        print "<hr />"
        print "<h2>Diagnostics:</h2><pre>"
        print self.MakeDiagnostics()
        print "</pre>"
        print "<hr/>"
        cgi.print_environ()
        print "</body></html>"
        
    def MakeDiagnostics(self):
        """Hook into debugging version of UnhandledError. This
        version prints values of all the (data) attributes of the
        application object. You can redefine this in subclasses to get
        other useful debugging information.  It should return an
        ordinary string (which may consist of many lines).
        """
        items = self.__dict__.items()
        items = map(str, items)
        return string.join(items, "\n")
                
    def DataTooLarge(self, size):
        """Send a message saying the data size was too big.
        A production implementation should redefine this method.
        """
        value = "Too much data: "+ str(size)
        send_error("Data size error", value)
        raise CGIQuit() # abort

    def MissingValue(self, field):
        """Send an error message about missing data.
        A production implementation should redefine this.
        """
        send_error("Missing data error", field+" not defined")
        raise CGIQuit()
        
    def AlarmHandler(self, number, traceback):
        """Send a message saying it took to long to read input.
        A production implementation should redefine this.
        """
        send_error("Timeout", "It took too long to read the data")
        raise CGIQuit()
    
