[Zope-Checkins] CVS: Zope3/lib/python/Zope/Publisher/Browser - IBrowserApplicationRequest.py:1.1.2.1 IBrowserGetPublication.py:1.1.2.1 Request.py:1.1.2.1 Response.py:1.1.2.1

Jim Fulton jim@zope.com
Sat, 16 Mar 2002 09:44:02 -0500


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

Added Files:
      Tag: Zope-3x-branch
	IBrowserApplicationRequest.py IBrowserGetPublication.py 
	Request.py Response.py 
Log Message:
Checking in partial publisher refactoring on the
Zope3-publisher-refactor-branch branch to facilitate collaboration
with Stephan.



=== Added File Zope3/lib/python/Zope/Publisher/Browser/IBrowserApplicationRequest.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""

Revision information:
$Id: IBrowserApplicationRequest.py,v 1.1.2.1 2002/03/16 14:44:00 jim Exp $
"""

from Zope.Publisher.IApplicationRequest import IApplicationRequest

class IHTTPApplicationRequest(IApplicationRequest):
    """HTTP request data.
    
    This object provides access to request data.  This includes, the
    input headers, form data, server data, and cookies.

    Request objects are created by the object publisher and will be
    passed to published objects through the argument name, REQUEST.

    The request object is a mapping object that represents a
    collection of variable to value mappings.  In addition, variables
    are divided into four categories:

      - Environment variables

        These variables include input headers, server data, and other
        request-related data.  The variable names are as <a
        href="http://hoohoo.ncsa.uiuc.edu/cgi/env.html">specified</a>
        in the <a
        href="http://hoohoo.ncsa.uiuc.edu/cgi/interface.html">CGI
        specification</a>

      - Cookies

        These are the cookie data, if present.

      - Other

        Data that may be set by an application object.

    The form data of a request is actually a Field Storage
    object.  When file uploads are used, this provides a richer and
    more complex interface than is provided by accessing form data as
    items of the request.  See the FieldStorage class documentation
    for more details.

    The request object may be used as a mapping object, in which case
    values will be looked up in the order: environment variables,
    other variables, and then cookies.    
    """


=== Added File Zope3/lib/python/Zope/Publisher/Browser/IBrowserGetPublication.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""

Revision information:
$Id: IBrowserGetPublication.py,v 1.1.2.1 2002/03/16 14:44:00 jim Exp $
"""

from Zope.Publisher.IPublication import IPublication

class IBrowserGetPublication (IPublication):
    """
    Object publication framework.
    """

    def getDefaultTraversal(request, ob):
        """Get the default published object for the request
        
        Allows a default view to be added to traversal.
        Returns (ob, steps_reversed).
        """



=== Added File Zope3/lib/python/Zope/Publisher/Browser/Request.py === (475/575 lines abridged)
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
##############################################################################
"""

$Id: Request.py,v 1.1.2.1 2002/03/16 14:44:00 jim Exp $
"""

from Zope.Publisher.HTTP import HTTPRequest

class BrowserRequest(HTTPRequest):

    __implements__ = HTTPRequest.__implements__, IHTTPCredentials, IHTTPRequest


    __auth = None          # The value of the HTTP_AUTHORIZATION header.
    __computed_urls = ()   # Names of computed URLx variables
    __script = ()          # SERVER_URL + __script + quoted_steps == full URL
    __server_script = ''   # __server_script + quoted_steps = full URL
    __server_url = ''      # Possible munged server_url

    def __init__(self, body_instream, outstream, environ):

        super(BrowserRequest, self).__init__(body_instream, outstream, environ)

        self.__steps = []
        self.__quoted_steps = []
        self._orig_env = environ
        environ = sane_environment(environ)

        if environ.has_key('HTTP_AUTHORIZATION'):
            self.__auth = environ['HTTP_AUTHORIZATION']
            del environ['HTTP_AUTHORIZATION']

        self.__environ=environ
        get_env=environ.get
        other = self.other
        self.__form={}

        ################################################################
        # Get base info first. This isn't likely to cause
        # errors and might be useful to error handlers.

[-=- -=- -=- 475 lines omitted -=- -=- -=-]

            key=key[9:]
        dict[key]=val
    if dict.has_key('HTTP_CGI_AUTHORIZATION'):
        dict['HTTP_AUTHORIZATION']=dict['HTTP_CGI_AUTHORIZATION']
        try: del dict['HTTP_CGI_AUTHORIZATION']
        except: pass
    return dict




def parse_cookie(
    text,
    result=None,
    qparmre=re.compile(
    '([\x00- ]*([^\x00- ;,="]+)="([^"]*)"([\x00- ]*[;,])?[\x00- ]*)'),
    parmre=re.compile(
    '([\x00- ]*([^\x00- ;,="]+)=([^\x00- ;,"]*)([\x00- ]*[;,])?[\x00- ]*)'),
    ):

    if result is None: result={}
    already_have=result.has_key

    mo_q = qparmre.match(text)

    if mo_q:
        # Match quoted correct cookies

        l     = len(mo_q.group(1))
        name  = mo_q.group(2)
        value = mo_q.group(3)

    else:
        # Match evil MSIE cookies ;)

        mo_p = parmre.match(text)

        if mo_p:
            l     = len(mo_p.group(1))
            name  = mo_p.group(2)
            value = mo_p.group(3)

        else: 
            return result 

    if not already_have(name): result[name]=value

    return apply(parse_cookie,(text[l:],result))




=== Added File Zope3/lib/python/Zope/Publisher/Browser/Response.py ===
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.

'''HTTP Response Output formatter

$Id: Response.py,v 1.1.2.1 2002/03/16 14:44:00 jim Exp $'''
__version__='$Revision: 1.1.2.1 $'[11:-2]

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

from Zope.Publisher.BaseResponse import BaseResponse
from Zope.Publisher.Exceptions import Redirect

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 = [n.lower() for n in dir(__builtins__) if n.endswith('Error')]
for name in en:
    status_codes[name] = 500


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



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 object 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 = None
    base = None
    realm = 'Zope'
    _error_format = 'text/html'
    payload = None
    _wrote_headers = 0
    _streaming = 0
    status = 200
    reason = 'Ok'


    def __init__(self, payload, outstream, header_output=None):
        self.payload = payload
        self._orig_payload = payload
        self.header_output = header_output
        BaseResponse.__init__(self, outstream)

    def retry(self):
        """
        Returns a response object to be used in a retry attempt
        """
        return self.__class__(self._orig_payload, self.outstream,
                              self.header_output)

    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 isinstance(status, 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.reason = 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.'''
        accum = self.accumulated_headers
        if not accum:
            self.accumulated_headers = accum = []
        accum.append('%s: %s' % (name, value))

    def setBody(self, body):
        return self.payload.setBody(self, body)

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

    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\r\n\t%s" % (h,delimiter,value)
        else: h=value
        self.setHeader(name,h)

    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):
        """
        """
        self.payload.handleException(self, exc_info)

    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 getHeaders(self):
        """
        Returns a mapping of correctly-cased header names to values.
        """
        res = {}
        headers = self.headers

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

        res["X-Powered-By"] = "Zope (www.zope.org), Python (www.python.org)"

        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)
            res[key] = val

        return res


    def getHeaderText(self, m):
        lst = ['Status: %s %s' % (self.status, self.reason)]
        lst.extend(map(lambda x: '%s: %s' % x, m.items()))
        lst.extend(self._cookie_list())
        accum = self.accumulated_headers
        if accum:
            lst.extend(accum)
        return ('%s\r\n\r\n' % '\r\n'.join(lst))


    def outputHeaders(self):
        m = self.getHeaders()
        header_output = self.header_output
        if header_output is not None:
            # Use the IHeaderOutput interface.
            header_output.setResponseStatus(self.status, self.reason)
            header_output.setResponseHeaders(m)
            cookies = self._cookie_list()
            if cookies:
                header_output.appendResponseHeaders(cookies)
            accum = self.accumulated_headers
            if accum:
                header_output.appendResponseHeaders(accum)
        else:
            # Write directly to outstream.
            s = self.getHeaderText(m)
            self.outstream.write(s)


    def __str__(self):
        """
        Debugging output.  Does not include headers added for connection
        control.
        """
        m = self.getHeaders()
        return self.getHeaderText(m) + self.body


    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 streaming:
            self._streaming = 1
        self.output(data)

    def output(self, data):
        if not self._wrote_headers:
            self.outputHeaders()
            self._wrote_headers = 1
        self.outstream.write(data)

    def outputBody(self):
        """
        Outputs the response body.
        """
        self.output(self.body)