##############################################################################
# 
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
# 
# Copyright (c) Digital Creations.  All rights reserved.
# 
# This license has been certified as Open Source(tm).
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions, and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
# 
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
# 
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
# 
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
# 
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
# 
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
# 
# 
# Disclaimer
# 
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
#   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
#   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.
# 
# 
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
# 
##############################################################################

__version__='$Revision: 1.17 $'[11:-2]

import re, string
from string import lower, rfind, split, strip, join, upper, find
from BaseRequest import BaseRequest
from HTTPResponse import HTTPResponse
from cgi import FieldStorage
from urllib import quote, unquote
from types import ListType

isCGI_NAME = {
        'SERVER_SOFTWARE' : 1, 
        'SERVER_NAME' : 1, 
        'GATEWAY_INTERFACE' : 1, 
        'SERVER_PROTOCOL' : 1, 
        'SERVER_PORT' : 1, 
        'REQUEST_METHOD' : 1, 
        'PATH_INFO' : 1, 
        'PATH_TRANSLATED' : 1, 
        'SCRIPT_NAME' : 1, 
        'QUERY_STRING' : 1, 
        'REMOTE_HOST' : 1, 
        'REMOTE_ADDR' : 1, 
        'AUTH_TYPE' : 1, 
        'REMOTE_USER' : 1, 
        'REMOTE_IDENT' : 1, 
        'CONTENT_TYPE' : 1, 
        'CONTENT_LENGTH' : 1,
        'SERVER_URL': 1,
        }.has_key

hide_key={'HTTP_AUTHORIZATION':1,
          'HTTP_CGI_AUTHORIZATION': 1,
          }.has_key


class HTTPRequest(BaseRequest):
    """\
    Model 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>

      - Form data

        These are data extracted from either a URL-encoded query
        string or body, if present.

      - Cookies

        These are the cookie data, if present.

      - Other

        Data that may be set by an application object.

    The form attribute 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,
    form data, and then cookies.
    """
    _file=None

    def __init__(self, stdin, environ):
        self.stdin = stdin
        self.environ = environ
        self.form = {}
        self.session = None
        self.response = HTTPResponse()

        if environ.has_key('HTTP_AUTHORIZATION'):
            self._auth=environ['HTTP_AUTHORIZATION']
            response._auth=1
            del environ['HTTP_AUTHORIZATION']

        if (lower(environ.get('HTTPS', "off")) == "on" or
                environ.get('SERVER_PORT_SECURE', 0)):
            self.scheme = "https"
        else:
            self.scheme = "http"

        ################################################################
        # Cookie values should *not* be appended to existing form
        # vars with the same name - they are more like default values
        # for names not otherwise specified in the form.
        cookies={}
        k = self.environ.get('HTTP_COOKIE','')
        if k:
            parse_cookie(k, cookies)
        self.cookies=cookies

        
    def processInputs(self):
        """Process request inputs

        We need to delay input parsing so that it is done under
        publisher control for error handling purposes.
        """
        environ=self.environ
        method = self.environ.get('REQUEST_METHOD','GET')
        
        if method != 'GET':
            fp=self.stdin
        else:
            fp=None

        fs=FieldStorage(fp=fp, environ=self.environ, keep_blank_values=1)
        if not hasattr(fs,'list') or fs.list is None:
            self._file=fs.file
        else:
            for item in fs.list:
                key=item.name
                if (hasattr(item,'file') and hasattr(item,'filename')
                    and hasattr(item,'headers')):
                    if (item.file and
                        (item.filename is not None
                         # RFC 1867 says that all fields get a content-type.
                         # or 'content-type' in map(lower, item.headers.keys())
                         )):
                        item=FileUpload(item)
                    else:
                        item=item.value

                # Filter out special names from form:
                if isCGI_NAME(key) or key[:5]=='HTTP_':
                    continue

                #Insert in dictionary
                if self.form.has_key(key):
                    # it is not a record or list of records
                    found=self.form[key]
                    if type(found) is ListType:
                        found.append(item)
                    else:
                        found=[found,item]
                        self.form[key]=found
                else:
                    self.form[key]=item
                                
    def get_header(self, name, default=None):
        """Return the named HTTP header, or an optional default
        argument or None if the header is not found. Note that
        both original and CGI-ified header names are recognized,
        e.g. 'Content-Type', 'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE'
        should all return the Content-Type header, if available.
        """
        environ=self.environ
        name=upper(join(split(name,"-"),"_"))
        val=environ.get(name, None)
        if val is not None:
            return val
        if name[:5] != 'HTTP_':
            name='HTTP_%s' % name
        return environ.get(name, default)


    # The following environmental variables are useful for reconstructing
    # the original URL, all of which are specified by CGI 1.1:
    #
    #   SERVER_NAME            "www.example.com"
    #   SCRIPT_NAME            "/q"
    #   PATH_INFO              "/debug/dump_sessions"
    #   QUERY_STRING           "session_id=10.27.8.40...."

    def get_server(self):
        """Return the server name with an optional port number."""

        server_name = strip(self.environ["SERVER_NAME"])
        server_port = self.environ.get("SERVER_PORT")

        if (not server_port or
                (self.scheme == "http" and server_port == "80") or
                (self.scheme == "https" and server_port == "443")):
            return server_name
        else:
            return server_name + ":" + server_port

    def get_path(self, n=0):
        """Return the path of the current request, chopping off 'n' path
        components from the right.  Eg. if the path is "/bar/baz/qux", n=0
        would return "/bar/baz/qux" and n=2 would return "/bar".  Note
        that the query string is not included.  Also note the handling of
        a path with a trailing slash.  If the original path is "/bar/baz/"
        then get_path(0) == "/bar/baz/" and get_path(1) == "/bar/baz/"
        (ie. the last component of a path with a trailing slash is the
         empty string).
        """
        path = "%(SCRIPT_NAME)s%(PATH_INFO)s" % self.environ
        if n == 0:
            return path
        else:
            path_comps = string.split(path, '/')
            if n > len(path_comps)-1:
                raise ValueError, "n=%d too big for path '%s'" % (n, path)
            return string.join(path_comps[:-n], '/')

    def get_url(self, n=0):
        """Return the URL of the current request, chopping off 'n' path
        components from the right.  Eg. if the URL is
        "http://foo.com/bar/baz/qux", n=2 would return
        "http://foo.com/bar".  Does not include the query string (if any).
        """
        return "%s://%s%s" % (self.scheme, self.get_server(), self.get_path(n))

    def get_environ (self, key, default=None):
        """get_environ(key : string) -> string

        Fetch a CGI environment variable from the request environment.
        See http://hoohoo.ncsa.uiuc.edu/cgi/env.html
        for the variables specified by the CGI standard.
        """
        return self.environ.get(key, default)

    def get(self, key, default=None):
        if self.environ.has_key(key):
            return self.environ[key]
        elif self.form.has_key(key):
            return self.form[key]
        else:
            return default

    def dump_html(self):
        result="<h3>form</h3><table>"
        row='<tr valign="top" align="left"><th>%s</th><td>%s</td></tr>'
        for k,v in self.form.items():
            result=result + row % (html_quote(k), html_quote(v))
        result=result+"</table><h3>cookies</h3><table>"
        for k,v in self.cookies.items():
            result=result + row % (html_quote(k), html_quote(v))
    
        result=result+"</table><h3>environ</h3><table>"
        for k,v in self.environ.items():
            if not hide_key(k):
                result=result + row % (html_quote(k), html_quote(v))
        return result+"</table>"

    def dump(self):
        result=""
        row='%-15s %s\n'
        L = self.form.items() ; L.sort()
        for k,v in L:
            result=result + row % (k,v)
        result=result+"\nCookies:\n"
        L = self.cookies.items() ; L.sort()
        for k,v in L:
            result=result + row % (k,v)

        result=result+"\nEnvironment:\n"
        L = self.environ.items() ; L.sort()
        for k,v in L:
            if not hide_key(k):
                result=result + row % (k,v)
        return result

    def browser_version (self):
        """Examine the User-agent request header to try to figure out
           what the current browser is.  Returns a (name,version) tuple
           where each element is a string or None if we couldn't parse the
           User-agent header.  Note that Netscape {Communicator,Navigator}
           and Internet Explorer advertise themselves as, respectively,
           "Mozilla" and "MSIE"."""

        ua = self.get_header ('user-agent')
        if ua is None:
            return (None, None)

        # Looks like Netscape...
        moz_match = re.search (r'(Mozilla)/([\w\.]+)', ua)
        if moz_match:
            # ...but smells like Microsoft
            ie_match = re.search (r'(MSIE)\s+([\w\.]+)', ua)
            if ie_match:
                return ie_match.group (1, 2)

            # nope, it's really Netscape
            else:
                return moz_match.group (1, 2)

        # neither Netscape or IE -- try a broader regex
        else:
            match = re.search (r'([^\s\/]+)/([\w\.]+)', ua)
            if match:
                return match.group (1, 2)
            else:
                return (None, None)

    # browser_version ()
    
    def _authUserPW(self):
        global base64
        auth=self._auth
        if auth:
            if lower(auth[:6]) == 'basic ':
                if base64 is None: import base64
                [name,password] = split(
                    base64.decodestring(split(auth)[-1]), ':')
                return name, password

    def redirect(self, location, permanent=0):
        """Create a redirection response.  If the location does not
        specify the server then it is automatically added.  The return
        value is a HTML document indicating the new URL (useful if the
        client browser does not honor the redirect).
        """
        if string.find(location, "://") == -1:
            location = "%s://%s%s" % (self.scheme, self.get_server(), location)
        return self.response.redirect(location, permanent)


base64=None


# This is duplicated from DocumentTemplate.DT_Util to
# prevent a dependency on the DocumentTemplate package.
# Some folks still use the ZPublisher package as a
# standalone publisher without DocumentTemplate.
def html_quote(value, character_entities=(
                      (('&'), '&amp;'),
                      (("<"), '&lt;' ),
                      ((">"), '&gt;' ),
                      (('"'), '&quot;'))): #"
        text=str(value)
        for re, name in character_entities:
            if find(text, re) >= 0: text=join(split(text, re), name)
        return text


def str_field(v):
    if type(v) is ListType:
        return map(str_field,v)

    if hasattr(v,'__class__') and v.__class__ is FieldStorage:
        v=v.value
    elif type(v) is not StringType:
        if hasattr(v,'file') and v.file: v=v.file
        elif hasattr(v,'value'): v=v.value
    return v


class FileUpload:
    '''\
    File upload objects

    File upload objects are used to represent file-uploaded data.

    File upload objects can be used just like files.

    In addition, they have a 'headers' attribute that is a dictionary
    containing the file-upload headers, and a 'filename' attribute
    containing the name of the uploaded file.
    '''

    def __init__(self, aFieldStorage):

        file=aFieldStorage.file
        if hasattr(file, '__methods__'): methods=file.__methods__
        else: methods= ['close', 'fileno', 'flush', 'isatty',
                        'read', 'readline', 'readlines', 'seek',
                        'tell', 'truncate', 'write', 'writelines']

        d=self.__dict__
        for m in methods:
            if hasattr(file,m): d[m]=getattr(file,m)

        self.headers=aFieldStorage.headers
        self.filename=aFieldStorage.filename
    

# XXX this should be converted to use re
import regex

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

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

    try:
        if qparmre.match(text) >= 0:
            # Match quoted correct cookies
            name=qparmre.group(2)
            value=qparmre.group(3)
            l=len(qparmre.group(1))
        elif parmre.match(text) >= 0:
            # Match evil MSIE cookies ;)
            name=parmre.group(2)
            value=parmre.group(3)
            l=len(parmre.group(1))
        else:
            # this may be an invalid cookie.
            # We'll simply bail without raising an error
            # if the cookie is invalid.
            return result
            
    finally: pass

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

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

# add class
class record:
    def __getattr__(self, key, default=None):
        if key in ('get', 'keys', 'items', 'values', 'copy', 'has_key'):
            return getattr(self.__dict__, key)
        raise AttributeError, key

    def __getitem__(self, key):
        return self.__dict__[key]
            
    def __str__(self):
        L1 = self.__dict__.items()
        L1.sort()
        return join(map(lambda item: "%s: %s" %item, L1), ", ") 

    __repr__ = __str__

