[Zope-Checkins] CVS: Zope3/lib/python/Zope/Publisher/HTTP - HTTPResponse.py:1.1.2.1

Shane Hathaway shane@digicool.com
Thu, 15 Nov 2001 13:07:05 -0500


Update of /cvs-repository/Zope3/lib/python/Zope/Publisher/HTTP
In directory cvs.zope.org:/tmp/cvs-serv11560/HTTP

Added Files:
      Tag: Zope-3x-branch
	HTTPResponse.py 
Log Message:
- Refined exception handling.
- Added HTTPResponse and DefaultPublication object.
(all untested as yet)


=== Added File Zope3/lib/python/Zope/Publisher/HTTP/HTTPResponse.py ===

'''HTTP Response Output formatter

$Id: HTTPResponse.py,v 1.1.2.1 2001/11/15 18:07:04 shane Exp $'''
__version__='$Revision: 1.1.2.1 $'[11:-2]

import string, sys, re
from types import StringType, ClassType
from cgi import escape

from Zope.Publisher.BaseResponse import BaseResponse

nl2sp = string.maketrans('\n',' ')

status_reasons={
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested range not satisfiable',
417: 'Expectation Failed',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not supported',
507: 'Insufficient Storage',
}

status_codes={}
# Add mappings for builtin exceptions and
# provide text -> error code lookups.
for key, val in status_reasons.items():
    status_codes[val.replace(' ', '').lower()] = key
    status_codes[val.lower()] = key
    status_codes[key]=key
    status_codes[str(key)]=key
en=filter(lambda n: n[-5:]=='Error', dir(__builtins__))
for name in map(string.lower, en):
    status_codes[name]=500
status_codes['nameerror']=503
status_codes['keyerror']=503
status_codes['redirect']=300


start_of_header_search=re.compile('(<head[^>]*>)', re.IGNORECASE).search

accumulate_header={'set-cookie': 1}.has_key


latin1_alias_match=re.compile(
    r'text/html(\s*;\s*charset=((latin)|(latin[-_]?1)|'
    r'(cp1252)|(cp819)|(csISOLatin1)|(IBM819)|(iso-ir-100)|'
    r'(iso[-_]8859[-_]1(:1987)?)))?$',re.I).match

def is_text_html(content_type):
    return (content_type == 'text/html' or
            latin1_alias_match(content_type) is not None)
                 

class HTTPResponse (BaseResponse):
    """
    An object representation of an HTTP response.
    
    The Response type encapsulates all possible responses to HTTP
    requests.  Responses are normally created by the object publisher.
    A published object may recieve the response abject as an argument
    named 'RESPONSE'.  A published object may also create it's own
    response object.  Normally, published objects use response objects
    to:

    - Provide specific control over output headers,

    - Set cookies, or

    - Provide stream-oriented output.

    If stream oriented output is used, then the response object
    passed into the object must be used.
    """

    accumulated_headers = ''
    body = ''
    realm = 'Zope'
    _error_format = 'text/html'

    def __init__(self, outstream, body='', headers=None,
                 status=None, cookies=None):
        BaseResponse.__init__(self, outstream, body, headers, status, cookies)

    def retry(self):
        """
        Returns a response object to be used in a retry attempt
        """
        return self.__class__(self.outstream)
    
    def setStatus(self, status, reason=None):
        '''
        Sets the HTTP status code of the response; the argument may
        either be an integer or a string from { OK, Created, Accepted,
        NoContent, MovedPermanently, MovedTemporarily,
        NotModified, BadRequest, Unauthorized, Forbidden,
        NotFound, InternalError, NotImplemented, BadGateway,
        ServiceUnavailable } that will be converted to the correct
        integer value.
        '''
        if status is None:
            status = 200
        else:
            if type(status) is StringType:
                status = status.lower()
            if status_codes.has_key(status):
                status=status_codes[status]
            else:
                status=500
            self.status=status
        if reason is None:
            if status == 200:
                reason = 'Ok'
            elif status_reasons.has_key(status):
                reason = status_reasons[status]
            else:
                reason = 'Unknown'
        self.setHeader('Status', "%d %s" % (status,str(reason)))
        self.errmsg = reason

    def setHeader(self, name, value, literal=0):
        '''
        Sets an HTTP return header "name" with value "value", clearing
        the previous value set for the header, if one exists. If the
        literal flag is true, the case of the header name is preserved,
        otherwise word-capitalization will be performed on the header
        name on output.
        '''
        key = name.lower()
        if accumulate_header(key):
            self.addHeader(name, value)
        else:
            name = literal and name or key
            self.headers[name]=value

    __setitem__ = setHeader

    def addHeader(self, name, value):
        '''
        Sets a new HTTP return header with the given value, while retaining
        any previously set headers with the same name.'''
        self.accumulated_headers=(
            "%s%s: %s\n" % (self.accumulated_headers, name, value))

    def setBody(self, body):
        '''
        Sets the body of the response
        
        Sets the return body equal to the (string) argument "body". Also
        updates the "content-length" return header.

        If the body is a 2-element tuple, then it will be treated
        as (title,body)
        
        If is_error is true then the HTML will be formatted as a Zope error
        message instead of a generic HTML page.
        '''
        body = str(body)

        if not self.headers.has_key('content-type'):
            c = (self.isHTML(body) and 'text/html' or 'text/plain')
            self.setHeader('content-type', c)

        content_type = self.headers['content-type']
        if is_text_html(content_type):
            # Some browsers interpret certain characters in Latin 1 as html
            # special characters. These cannot be removed by html_quote,
            # because this is not the case for all encodings.
            body = body.replace('\213', '&lt;')
            body = body.replace('\233', '&gt;')

        self.body = body
        self.insertBase()
        self.updateContentLength()
        return self

    def updateContentLength(self):
        blen = str(len(self.body))
        if blen[-1:] == 'L':
            blen = blen[:-1]
        self.setHeader('content-length', blen)

    def setBase(self,base):
        'Sets the base URL for the returned document.'
        if base[-1:] != '/':
            base=base+'/'
        self.base=base

    def insertBase(self,
                   base_re_search=re.compile('(<base.*?>)',re.I).search
                   ):

        # Only insert a base tag if content appears to be html.
        content_type = self.headers.get('content-type', '').split(';')[0]
        if content_type and content_type != 'text/html':
            return

        if self.base:
            body=self.body
            if body:
                match=start_of_header_search(body)
                if match is not None:
                    index=match.start(0) + len(match.group(0))
                    ibase=base_re_search(body)
                    if ibase is None:
                        self.body=('%s\n<base href="%s" />\n%s' %
                                   (body[:index], self.base, body[index:]))

    def appendToCookie(self, name, value):
        '''
        Creates an HTTP header that sets a cookie on cookie-enabled
        browsers with a key "name" and value "value". If a value for the
        cookie has previously been set in the response object, the new
        value is appended to the old one separated by a colon. '''

        cookies=self.cookies
        if cookies.has_key(name): cookie=cookies[name]
        else: cookie=cookies[name]={}
        if cookie.has_key('value'):
            cookie['value']='%s:%s' % (cookie['value'], value)
        else: cookie['value']=value

    def expireCookie(self, name, **kw):
        '''
        Causes an HTTP cookie to be removed from the browser
        
        The response will include an HTTP header that will remove the cookie
        corresponding to "name" on the client, if one exists. This is
        accomplished by sending a new cookie with an expiration date
        that has already passed. Note that some clients require a path
        to be specified - this path must exactly match the path given
        when creating the cookie. The path can be specified as a keyword
        argument.
        '''
        dict={'max_age':0, 'expires':'Wed, 31-Dec-97 23:59:59 GMT'}
        for k, v in kw.items():
            dict[k]=v
        cookies=self.cookies
        if cookies.has_key(name):
            # Cancel previous setCookie().
            del cookies[name]
        self.setCookie(name, 'deleted', **dict)

    def setCookie(self, name, value, **kw):
        '''
        Sets an HTTP cookie on the browser

        The response will include an HTTP header that sets a cookie on
        cookie-enabled browsers with a key "name" and value
        "value". This overwrites any previously set value for the
        cookie in the Response object.
        '''
        cookies=self.cookies
        if cookies.has_key(name):
            cookie=cookies[name]
        else: cookie=cookies[name]={}
        for k, v in kw.items():
            cookie[k]=v
        cookie['value']=value

    def appendToHeader(self, name, value, delimiter=","):
        '''
        Appends a value to a header
        
        Sets an HTTP return header "name" with value "value",
        appending it following a comma if there was a previous value
        set for the header. '''
        headers=self.headers
        if headers.has_key(name):
            h=self.header[name]
            h="%s%s\n\t%s" % (h,delimiter,value)
        else: h=value
        self.setHeader(name,h)

    def isHTML(self, str):
        s = str.strip()
        return (s[:6].lower() == '<html>' or
                s[:14].lower() == '<!doctype html')

    def redirect(self, location, status=302):
        """Causes a redirection without raising an error"""
        self.setStatus(status)
        self.setHeader('Location', location)
        return location

    def handleException(self, exc_info):
        """
        """
        t, v = exc_info[:2]
        if isinstance(t, ClassType):
            if issubclass(t, Redirect):
                self.redirect(v.getLocation())
                return
        self.setStatus(t)
        body = ("<html><head><title>Site error</title></head>\n"
                "<body><p>A site error occurred.</p><pre>%s</pre>"
                "</body></html>" %
                escape(traceback_string(t, v, exc_info[2]))
                )
        self.setBody(body)


    _wrote=None

    def _cookie_list(self):
        cookie_list=[]
        for name, attrs in self.cookies.items():

            # Note that as of May 98, IE4 ignores cookies with
            # quoted cookie attr values, so only the value part
            # of name=value pairs may be quoted.

            cookie='Set-Cookie: %s="%s"' % (name, attrs['value'])
            for name, v in attrs.items():
                name = name.lower()
                if name=='expires': cookie = '%s; Expires=%s' % (cookie,v)
                elif name=='domain': cookie = '%s; Domain=%s' % (cookie,v)
                elif name=='path': cookie = '%s; Path=%s' % (cookie,v)
                elif name=='max_age': cookie = '%s; Max-Age=%s' % (cookie,v)
                elif name=='comment': cookie = '%s; Comment=%s' % (cookie,v)
                elif name=='secure' and v: cookie = '%s; Secure' % cookie
            cookie_list.append(cookie)

        # Should really check size of cookies here!
        
        return cookie_list

    def __str__(self,
                html_search=re.compile('<html>',re.I).search,
                ):
        """
        Outputs the headers of the response.
        """
        if self._wrote: return ''       # Streaming output was used.

        headers=self.headers
        body=self.body

        if (not headers.has_key('content-length') and
            not headers.has_key('transfer-encoding')):
            self.updateContentLength()

        headersl=[]
        append=headersl.append

        # status header must come first.
        append("Status: %s" % headers.get('status', '200 OK'))
        append("X-Powered-By: Zope (www.zope.org), Python (www.python.org)")
        if headers.has_key('status'):
            del headers['status']
        for key, val in headers.items():
            if key.lower() == key:
                # only change non-literal header names
                key="%s%s" % (key[:1].upper(), key[1:])
                start=0
                l=key.find('-',start)
                while l >= start:
                    key="%s-%s%s" % (key[:l],key[l+1:l+2].upper(),key[l+2:])
                    start=l+1
                    l=key.find('-',start)
            append("%s: %s" % (key, val))
        if self.cookies:
            headersl=headersl+self._cookie_list()
        headersl[len(headersl):]=[self.accumulated_headers, body]
        return '\n'.join(headersl)

    def write(self,data):
        """
        Return data as a stream

        HTML data may be returned using a stream-oriented interface.
        This allows the browser to display partial results while
        computation of a response to proceed.

        The published object should first set any output headers or
        cookies on the response object.

        Note that published objects must not generate any errors
        after beginning stream-oriented output. 

        """
        if not self._wrote:
            self.outputBody()
            self._wrote=1
            self.outstream.flush()

        self.outstream.write(data)



def format_exception(etype,value,tb,limit=None):
    import traceback
    result=['Traceback (innermost last):']
    if limit is None:
        if hasattr(sys, 'tracebacklimit'):
            limit = sys.tracebacklimit
    n = 0
    while tb is not None and (limit is None or n < limit):
        f = tb.tb_frame
        lineno = tb.tb_lineno
        co = f.f_code
        filename = co.co_filename
        name = co.co_name
        locals=f.f_locals
        result.append('  File %s, line %d, in %s'
                      % (filename,lineno,name))
        try: result.append('    (Object: %s)' %
                           locals[co.co_varnames[0]].__name__)
        except: pass
        try: result.append('    (Info: %s)' %
                           str(locals['__traceback_info__']))
        except: pass
        tb = tb.tb_next
        n = n+1
    result.append(' '.join(traceback.format_exception_only(etype, value)))
    return result


def traceback_string(t,v,tb):
    tb=format_exception(t,v,tb,200)
    return '\n'.join(tb)