########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Http/Basic.py,v 1.108 2005/04/06 06:19:15 mbrown Exp $
"""
4Suite API module for the HTTP Handler

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

__all__ = ['BasicHttpHandler', 'XmlBody']

import re, urlparse, os, time, cgi, sha, cgi
try:
    # Python 2.3+
    from types import basestring as StringTypes
except ImportError:
    from types import StringTypes

import BaseRequestHandler
import HeaderDict
import Status
import Session

from Ft.Lib import Time, Uri
from Ft.Server import RESERVED_NAMESPACE
from Ft.Server.Common import Schema, ResourceTypes
from Ft.Server.Common.ResourceTypes import *
from Ft.Server.Server import FtServerServerException
from Ft.Server.Server import Error as ServerError
from Ft.Server.Server import SCore
from Ft.Server.Server.Drivers.PathImp import RepoPathToUri
from Ft.Server.Server.Http import FtServerHttpException, Error
from Ft.Server.Server.SCore import GetRepository
from Ft.Xml.Lib.XmlString import IsQName
from Ft.Server.Server.Drivers import FtssInputSource

REPL_PAT = re.compile(r"(?<!\\)\\([0-9])")

class BasicHttpHandler(BaseRequestHandler.BaseRequestHandler):

    def __init__(self, connection):
        BaseRequestHandler.BaseRequestHandler.__init__(self, connection)
        self.isSoap = self.isXmlBody = False
        config = vars(self.server)
        self.http_post_encoding_var = config.get('http_post_encoding_var', 'http-post-encoding')
        #prime config vars
        return

    # Now handled by BaseRequestHandler
    #def do_HEAD(self):
    #    body = self._process_request(self.args)
    #    if body is not None:
    #        self.headers_out['Content-Length'] = len(body)
    #    return

    def do_GET(self):
        body = self._process_request(self.args)
        # wfile is a buffering stream
        body and self.wfile.write(body)
        return

    def do_POST(self):
        content_type = self.headers_in['Content-Type'][0]
        #SOAP 1.2, strictly
        self.isSoap = content_type == 'application/soap'
        self.isXmlBody = re.match(r'(?:text|application)/.*\+?xml',
                                  content_type) is not None

        body = self._process_request(self.args)
        # wfile is a buffering stream
        body and self.wfile.write(body)
        return

    def _process_request(self, args):
        #First, load all of our config options
        #This will set up the appropriate instance variables
        #FIXME: this seems wasteful.  Isn't there a way to reload only if something has changed?
        self.load_config(args)
        #Now get a connection to the repo
        #There are four options here
        #1.  An existing session
        #2.  A new session
        #3.  A 403 auth
        #4.  An anon connection

        if self.getRepo():
            #Will have sent the error/response
            return False

        self.commit = True
        new_response_headers = HeaderDict.HeaderDict()
        status = 0
        try:
            try:
                #First of all, if there are no query args, try to match a
                #processing rule
                rule_applied = False
                if not self.args:
                    rule_applied, data = self.apply_rules()
                if rule_applied:
                    (res, chain, new_args) = data
                    args.update(new_args)
                else:
                    #If they specify an 'xslt' arg, use it, else, if there is a
                    #default for this directory or resource type use that.
                    if self.isXmlBody or self.isSoap:
                        res = XmlBody(self.body, self.repo)
                    else:
                        res = self.load_resource(not len(args))
                    if res is None:
                        #The response was handled
                        return
                    chain = self.load_stylesheets(res)

                #If there are stylesheets, then go into the render/action loop
                #If not just fetch the resource and exit
                if len(chain):
                    # prepare stylesheet params
                    # (usable by the stylesheet; not modifiable outside of XSLT processing)
                    #
                    params = {}
                    if self.isSoap:
                        params[(RESERVED_NAMESPACE, 'soap-action')] = self.headers_in.get('SOAPAction', [''])[0]

                    #FIXME: This is a hack.  We really need a parameter that
                    #properly represents the resource ID of the source doc
                    if not(res.__class__ == XmlBody):
                        params[(RESERVED_NAMESPACE, 'resource-id')] = RepoPathToUri(res.getAbsolutePath())
                    params[(RESERVED_NAMESPACE, 'uri-path')] = self.path
                    params[(RESERVED_NAMESPACE, 'http-host')] = self.hostname
                    params[(RESERVED_NAMESPACE, 'server-name')] = self.hostname
                    #FIXME: We need a more reliable way of getting the port than the incoming header
                    host = self.headers_in.get("Host", [""])[0]
                    host_list = host.split(":")
                    port = len(host_list) > 1 and host_list[1] or ""
                    params[(RESERVED_NAMESPACE, 'server-port')] = port
                    params[(RESERVED_NAMESPACE, 'absolute-request-uri')] = "http://" + host + self.unparsed_uri
                    #Deprecate request-uri, and remove it pre-1.0
                    params[(RESERVED_NAMESPACE, 'request-uri')] = params[(RESERVED_NAMESPACE, 'absolute-request-uri')]
                    params[(RESERVED_NAMESPACE, 'request')] = self.unparsed_uri
                    params[(RESERVED_NAMESPACE, 'user-name')] = self.user or BaseRequestHandler.ANONYMOUS_USER_NAME
                    params[(RESERVED_NAMESPACE, 'document-root')] = self.documentRoot or u''
                    if not(res.__class__ == XmlBody):
                        params[(RESERVED_NAMESPACE, 'source-uri')] = res.getAbsolutePath()[len(self.documentRoot or u''):]
                    #The stylesheet URI of the highest precedence stylesheet
                    #in that is first in the chain
                    main_xform = type(chain[0]) in [type([]), type(())] and chain[0][0] or chain[0]
                    params[(RESERVED_NAMESPACE, 'xslt-uri')] = main_xform.getAbsolutePath()[len(self.documentRoot or u''):]
                    #Holdover name from legacy
                    params[(RESERVED_NAMESPACE, 'stylesheet-uri')] = params[(RESERVED_NAMESPACE, 'xslt-uri')]
                    if self.session:
                        params[(RESERVED_NAMESPACE, 'sessionId')] = self.session.id
                    else:
                        params[(RESERVED_NAMESPACE, 'sessionId')] = ''
                    # query args are stylesheet params too
                    for name, value in args.items():
                        # colons and characters not allowed in
                        # XSLT parameter names become hyphens.
                        if name.find(u':') != -1:
                            name = name.replace(u':', u'-')
                        if not IsQName(name):
                            newname = u''
                            for c in name:
                                if not IsQName(c):
                                    c = u'-'
                                newname += c
                            name = newname
                            del newname
                        # Treat given parameters the same as 4XSLT does
                        # (if multiple values for same param, only use
                        # the first value)
                        if len(value) > 1:
                            params[name] = value
                        else:
                            params[name] = value[0]

                    # prepare extension params for extension elements and functions to use
                    # (they belong to the processor and can be modified)
                    #
                    ext_params = {
                        (RESERVED_NAMESPACE, 'commit'): 0,
                        (RESERVED_NAMESPACE, 'handler'): self,
                        (RESERVED_NAMESPACE, 'error-log'): self.server.errorLog,
                        (RESERVED_NAMESPACE, 'access-log'): self.server.accessLog,
                    }

                    #pass query arguments as a dictionary to extension parameters
                    ext_params[(RESERVED_NAMESPACE,'queryArgs')] = args
                    self.server.errorLog.debug("Begin Rendering")
                    #chain is a list of lists.  The outer list is the chain
                    #of stylesheets.  Each inner list represents all
                    #the transforms that make up each link in the chain
                    body, imt = res.applyXslt(chain, params,
                                              ignorePis=(not self.enable_pis),
                                              extParams=ext_params)
                    self.server.errorLog.debug("End Rendering")

                    if ext_params.get((RESERVED_NAMESPACE, 'rollback')):
                        self.server.errorLog.info("Ext Param Config Argument: rollback --> 1\n")
                        self.commit = False

                    redirect = ext_params.get((RESERVED_NAMESPACE, 'response-uri'))

                    login = ext_params.get((RESERVED_NAMESPACE, 'login'))
                    if login:
                        (user, password, ttl, success_redirect,
                         failure_redirect, timeout_redirect
                         ) = login
                        self.server.errorLog.debug("Session-based login requested via proc. ext. params for user %s" % user)
                        #Use *current* repo to check credentials because they
                        #may have added a user or changed a password in
                        #this tx
                        if self.repo.checkLogin(user, password):
                            failed = False
                        else:
                            failed = True
                            self.server.errorLog.error("Repository access denied for user %s; will redirect" % user)
                            redirect = failure_redirect
##                        #Now create a new repo for setting up the session
##                        #because we want to use the new username, not the
##                        #old one
##                        try:
##                            temp_repo = GetRepository(user, password,
##                                                      self.server.errorLog,
##                                                      self.server.properties)
##                            failed = False
##                        except FtServerServerException, e:
##                            failed = True
##                            if e.errorCode == ServerError.INVALID_LOGIN:
##                                redirect = failure_redirect
##                            else:
##                                raise
                        if not failed:
                            if self.repo.hasSession():
                                self.repo.invalidateSession()
                            sid = self.repo.createSession('', ttl or self.session_ttl, overriddenLogin=(user, password))
                            session = Session.Create(sid, self.session_method,
                                                     self.server.errorLog)
                            new_response_headers = HeaderDict.HeaderDict()
                            if session:
                                self.server.errorLog.info("Login successful; new session created; will redirect")
                                session.userAgentVariables['timeout-redirect'] = timeout_redirect
                                session.updateHeaders(
                                    self.repo, new_response_headers,
                                    self.server.errorLog
                                    )
                                self.sendRedirect(success_redirect,
                                                  new_response_headers)
                                #self.repo.txCommit()
                                return ''
                            else:
                                self.server.errorLog.error("Login successful, but error creating new session")

                    if redirect:
                        self.server.errorLog.info("Redirecting to: %s\n" % redirect)
                        new_response_headers = HeaderDict.HeaderDict()
                        if self.session:
                            self.session.updateHeaders(
                                self.repo, new_response_headers,
                                self.server.errorLog)
                        self.sendRedirect(redirect, new_response_headers)
                        return ''

                    #Fall through to std fallback
                    #charset = 'iso-8859-1'
                    charset = None
                    status = Status.HTTP_OK
                    if self.session:
                        self.session.updateHeaders(self.repo, self.headers_out,
                                                   self.server.errorLog)

                else:
                    # Always add a Last-Modified HTTP header to the response
                    # to help browsers cache entities.
                    # If the browser requested an entity only
                    #"If-Modified-Since" sometime after or equal to
                    # the entity's last modified date in 4SS, then respond
                    # with a 304 ("Not Modified").

                    #FIXME: rfc2068 sction 14.26 seems to indicate that
                    #we must handle "If-None-Match: *", even though that
                    #header would only really make sense for PUTs

                    last_modified = res.getLastModifiedDate()
                    #print "last mod:", repr(last_modified)

                    # Netscape 4 sends If-Modified-Since headers that have the
                    # full weekday name and "; extra crap" on the end
                    check_last_mod = self.headers_in.terse().get('If-Modified-Since')
                    if check_last_mod:
                        lmd = Time.FromISO8601(last_modified)
                        clm = Time.FromRFC822(check_last_mod.split(';')[0])

                        #print "lmd: ", lmd
                        #print "clm: ", clm
                        #print "cmp: ", lmd <= clm

                        if lmd <= clm:
                            #Do not add cookie headers for requests that
                            #don't involve them, since these do not concern
                            #app logic, and the 'expires' Set-Cookie field
                            #prevents browsers from caching.
                            #if self.session:
                            #    self.session.updateHeaders(self.repo, new_response_headers, self.server.errorLog)
                            self.headers_out.update(new_response_headers)
                            self.status = Status.HTTP_NOT_MODIFIED
                            return ''
                    #Set the Last-Modified Header, e.g.
                    # Mon, 17 Dec 2001 19:29:08 GMT
                    # and ensure it doesn't exceed the Date header
                    # as per RFC 2616 sec. 14.19.
                    dt = Time.FromISO8601(last_modified)
                    if dt.asPythonTime() > time.mktime(time.gmtime(self.request_time)):
                        dt = Time.FromPythonTime(self.request_time)
                    new_response_headers['Last-Modified'] = dt.asRFC822DateTime()
                    #print "Setting LMT: %s" % headers['Last-Modified']
                    body = res.getContent()
                    imt = res.getImt()

                    #charset = 'iso-8859-1'
                    charset = None
                    status = Status.HTTP_OK

            except FtServerServerException, e:
                #import sys, traceback; traceback.print_exc(1000, sys.stdout)
                self.commit = False
                if e.errorCode in [ServerError.UNKNOWN_PATH, ServerError.INVALID_PATH]:
                    self.server.errorLog.debug("Access to unknown path %s" % e.params['path'])
                    self.send_error(Status.HTTP_NOT_FOUND,
                                    uri=self.path)
                    return
                elif e.errorCode == ServerError.PERMISSION_DENIED:
                    #import traceback, cStringIO
                    #st = cStringIO.StringIO()
                    #traceback.print_exc(st)
                    #self.server.errorLog.debug(st.getvalue())
                    if not self.authChecker(mandatory=True):
                        self.server.errorLog.warning('Attempted Unauthorized "%(level)s" Access to: %(path)s'%e.params)

                        if self.session_method and self.session_permission_denied_uri:
                            self.sendRedirect(self.session_permission_denied_uri,
                                              HeaderDict.HeaderDict())
                        else:
                            # send_error will determine if the response should be HTTP_FORBIDDEN or HTTP_UNAUTHORIZED
                            self.send_error(Status.HTTP_FORBIDDEN, uri=self.path)
                    return
                raise
            except:
                self.commit = False
                raise
        finally:
            if self.repo is not None:
                if self.commit:
                    self.repo.txCommit()
                else:
                    self.repo.txRollback()
        # if we reached this point, there were no errors getting the resource
        # or transform result from the repo, and status is HTTP_OK, so send
        # the response line and headers, and return body to do_GET or do_POST
        if charset:
            content_type = '%s; charset=%s' % (imt, charset)
        else:
            content_type = imt
        new_response_headers['Content-Type'] = content_type

        #for name,value in headers.items():
        #    self.headers_out[name] = value
        self.headers_out.update(new_response_headers)
        self.status = status
        return body


    def load_config(self, args):
        #This is probably OK to do
        config = vars(self.server)

        #This is what config might look like:
        #config= {
        #         'accessLog': None
        #         'admin': u'root@localhost',
        #         'backlog': 512,
        #         'defaultXslt': {1: '/ftss/data/container.xslt'},
        #         'documentRoot': u'/ftss/demos',
        #         'Driver': {'TYPE': 'FlatFile', 'Root': u'xmlserver'},
        #                    'LogFile': u'\\_Dev\\4ss.log',
        #                    'LogLevel': ('debug', 7),
        #                    'TemporaryReapInterval': -1,
        #                    'DynamicReloadInterval': -1,
        #                    'ConfigFile': 'C:\\_dev\\4ss.conf',
        #                    'CoreId': u'Core'
        #                   },
        #         'errorFilename': None,
        #         'errorLog': <Ft.Server.Server.Lib.LogUtil.Logger instance at 012A11B4>,
        #         'handler': <class Ft.Server.Server.Http.Basic.RequestHandler at 00D39884>,
        #         'hostname': u'localhost',
        #         'logLevel': ('debug', 7),
        #         'names': [],
        #         'port': 8081,
        #         'properties': {'PidFile': u'\\_Dev\\4ss.pid',
        #         'redirects': {u'/ftss/demos': u'index.html'},
        #         'session_anonymous_flag': u'anonymous-login',
        #         'session_invalid_uri': u'/buyerbase/?xslt=home.xslt&expired=1',
        #         'session_method': u'Cookie',
        #         'session_password': u'password',
        #         'session_post_invalid_login_uri_qs': u'invalid-login',
        #         'session_post_login_uri_qs': u'post-login',
        #         'session_user_name': u'user-name',
        #        }

        configOptionsToLog = {}

        self.documentRoot = config.get('documentRoot')
        if self.documentRoot:
            configOptionsToLog['documentRoot'] = str(self.documentRoot)

        ##URI Query Path
        #The URI query path option allows us to override what path is used
        #As the source object in this request.  There are two options associated with it
        #1.  What path must be specified in the request URL to initiate this processing
        #2.  What query arg must be present to define the new source path
        self.uri_query_path = config.get('uri_query_path','/resourcebyuri')
        self.uri_param = config.get('uri_param','uri')
        self.uri_path = args.get(self.uri_param,[None])[0]
        configOptionsToLog['uri_query_path'] = str(self.uri_query_path)
        configOptionsToLog['uri_param'] = str(self.uri_param)
        configOptionsToLog['uri_path'] = str(self.uri_path)

        ##XSLT Param
        #The xslt param is used to specify what stylesheets are applied to the source object
        #It consists of a name of the param, and a list of stylesheets
        #Ignore PIS tells us the param to send to the processor
        self.xslt_param = config.get('xslt_param', 'xslt')
        self.stylesheets = args.get(self.xslt_param, [])
        configOptionsToLog['xslt_param'] = str(self.xslt_param)
        configOptionsToLog['stylesheets'] = str(self.stylesheets)

        self.enable_pis_param = config.get('enable_pis_param','enable-pis')
        self.enable_pis = self.enable_pis_param in args
        configOptionsToLog['enable_pis_param'] = str(self.enable_pis_param)
        configOptionsToLog['enable_pis'] = str(self.enable_pis)

        ###Sessions
        #To define how sessions work, there are many options.  First is what type of session are
        # supported.  Then, where is the user name and password for sesion found.  How is anon session
        #created.  What are the uris to redirect to on an invalid and valid login
        #lastly, what page is displayed when a session is invalidated
        self.session_method = config.get('session_method')
        configOptionsToLog['session_method'] = str(self.session_method)

        if self.session_method:
            self.session_user_name_qs = config.get('session_user_name', 'user-name')
            self.session_user_name = args.get(self.session_user_name_qs, [None])[0]
            configOptionsToLog['session_user_name (%s)' % self.session_user_name_qs] = str(self.session_user_name)

            self.session_password_qs = config.get('session_password','password')
            self.session_password = args.get(self.session_password_qs,[None])[0]
            configOptionsToLog['session_password (%s)' % self.session_password_qs] = str(self.session_password)

            self.session_ttl = int(config.get('session_ttl', '1800'))
            configOptionsToLog['session_ttl'] = str(self.session_ttl)

            self.session_anonymous_qs = config.get('session_anonymous_flag','anonymous-login')
            self.session_anonymous = len(args.get(self.session_anonymous_qs,[])) > 0
            configOptionsToLog['session_anonymous (%s)' % self.session_anonymous_qs] = str(self.session_anonymous)

            self.session_invalid_uri = config.get('session_invalid_uri',None)
            configOptionsToLog['session_invalid_uri'] = str(self.session_invalid_uri)

            self.session_post_login_uri_qs = config.get('session_post_login_uri_qs', None)
            self.session_post_login_uri = args.get(self.session_post_login_uri_qs, [None])[0]
            configOptionsToLog['session_post_login_uri (%s)' % self.session_post_login_uri_qs] = str(self.session_post_login_uri)

            self.session_post_invalid_login_uri_qs = config.get('session_post_invalid_login_uri_qs',None)
            self.session_post_invalid_login_uri = args.get(self.session_post_invalid_login_uri_qs,[None])[0]
            configOptionsToLog['session_post_invalid_login_uri (%s)' % self.session_post_invalid_login_uri_qs] = str(self.session_post_invalid_login_uri)

            self.session_permission_denied_uri = config.get('session_permission_denied_uri',None)
            configOptionsToLog['session_permission_denied_uri'] = str(self.session_permission_denied_uri)

        self.http_post_encoding_var = config.get('http_post_encoding_var', 'http-post-encoding')
        ###Actions
        #Give us a hint as to what to do at a high level
        self.action_qs = config.get('action-name', 'action-name')
        self.action = args.get(self.action_qs, ['pass'])[0]
        configOptionsToLog['action (%s)' % self.action_qs] = str(self.action)

        #Response URI is where we redirect to.  It is used with some of the actions.
        self.response_uri_qs = config.get('response_uri', 'response-uri')
        self.response_uri = args.get(self.response_uri_qs,[None])[0]
        configOptionsToLog['response_uri (%s)' % self.response_uri_qs] = str(self.response_uri)

        #Default XSLT allows us to set a stylesheet up for a resource type
        self.xsltDefaults = {}
        for resourceType, path in config['defaultXslt'].items():
            self.xsltDefaults[resourceType] = path
            configOptionsToLog['default XSLT for resource type %s' % Schema.g_rdfResourceTypes[resourceType]] = path

        #Set the polymorphic types here
        if ResourceTypes.ResourceType.DOCUMENT_DEFINITION in self.xsltDefaults:
            for rt in ResourceTypes.DOCUMENT_DEFINITIONS:
                if rt not in self.xsltDefaults:
                    self.xsltDefaults[rt] = self.xsltDefaults[ResourceTypes.ResourceType.DOCUMENT_DEFINITION]

        if ResourceTypes.ResourceType.CONTAINER in self.xsltDefaults:
            for rt in ResourceTypes.CONTAINERS:
                if rt not in self.xsltDefaults:
                    self.xsltDefaults[rt] = self.xsltDefaults[ResourceTypes.ResourceType.CONTAINER]

        if ResourceTypes.ResourceType.XML_DOCUMENT in self.xsltDefaults:
            for rt in ResourceTypes.XML_DOCUMENTS:
                if rt not in self.xsltDefaults:
                    self.xsltDefaults[rt] = self.xsltDefaults[ResourceTypes.ResourceType.XML_DOCUMENT]

        self.redirects = {}
        for src, dest in config['redirects'].items():
            self.redirects[src] = dest
            configOptionsToLog['redirect destination for %s' % src] = dest

        #Disable XSLT defaults is a query param that will override default processing
        self.disable_default_xslt_qs = config.get('disable_default_xslt_param', 'disable-default-xslt')
        self.disable_default_xslt = args.get(self.disable_default_xslt_qs,[None])[0]
        configOptionsToLog['disable_default_xslt (%s)' % self.disable_default_xslt_qs] = str(self.disable_default_xslt)

        #Processing rules provide generic server-side processing tools
        #In order for proper overriding of rules
        self.processing_rules = [
            #(pattern, src, xforms, args, pat_str, chain)
            (re.compile(r[0]), r[1], r[2], r[3], r[0], r[4] == 'yes')
                for r in config['processing_rules']
            ]
        self.processing_rules.reverse()

        #Log all the loaded config options
        self.server.errorLog.debug("Loaded config options from server config file and request query args:\n")
        keys = configOptionsToLog.keys()
        keys.sort()
        for optionkey in keys:
            self.server.errorLog.debug('    %s: %s\n' % (optionkey, configOptionsToLog.get(optionkey)))
        return

    def getRepo(self):
        """
        Return 1 if a response was already sent
        """
        self.repo = None
        self.session = None

        if self.action == 'login':
            ##Perform a login.  Get the query args user-name and password
            #Create a new session, then return the response uri

            if not self.session_method:
                raise FtServerHttpException(Error.SESSIONS_NOT_ENABLED,
                    login_query_arg=self.action_qs)

            if self.session_anonymous or (not self.session_user_name and not self.session_password):
                self.user = None
                self.password = None
                self.server.errorLog.debug("Session-based login requested for anonymous user")
            else:
                self.user = self.session_user_name
                self.password = sha.new(self.session_password or '').hexdigest()
                self.server.errorLog.debug("Session-based login requested for user %s" % self.user)

            try:
                # sendError=False will prevent a failed login from generating an error response
                # that would normally be for HTTP authentication; we will generate our own
                # response, if needed
                repo = self.getRepository(sendError=False)
            except FtServerServerException, e:
                if e.errorCode == ServerError.INVALID_LOGIN:
                    if not self.session_post_invalid_login_uri:
                        raise FtServerHttpException(Error.BAD_LOGIN_FAILURE_REDIRECT)

                    #Send back a 302
                    self.server.errorLog.error("Repository access denied for user %s; will redirect" % self.user)
                    self.sendRedirect(self.session_post_invalid_login_uri,
                                      HeaderDict.HeaderDict())
                    return True
                raise
            sid = repo.createSession('', self.session_ttl)
            self.session = Session.Create(sid, self.session_method,
                                          self.server.errorLog)

            if self.session_post_login_uri:
                self.server.errorLog.info("Login successful; new session created; will redirect")
                new_response_headers = HeaderDict.HeaderDict()
                if self.session:
                    self.session.updateHeaders(repo, new_response_headers,
                                               self.server.errorLog)
                self.sendRedirect(self.session_post_login_uri, new_response_headers)
                repo.txCommit()
                return True
            #If not, then they want to do more with this
            self.server.errorLog.info("Login successful; new session created; no redirect")
            self.repo = repo
            return False
        elif self.session_method:
            # Try to use the request headers to associate the request
            # with a session that has been stored in the repo.
            # This call will raise an exception if there is a sessionId
            # or login info in the request but no session in the repo.
            try:
                session = Session.Retrieve(self.args, self.headers_in,
                                           self.session_method,
                                           self.server.errorLog)
                if session:
                    self.session = session
                    #FIXME: Get client address and check for exceptions
                    self.repo = SCore.RetrieveSession(session.id, '',
                                                      self.server.errorLog,
                                                      self.server.properties)
                    self.server.errorLog.info("Retrieved Session for user %s"
                                               % self.repo.getUserName())
                    return False
            except FtServerServerException, e:
                if session and session.userAgentVariables.get('timeout-redirect'):
                    self.session_invalid_uri = session.userAgentVariables.get('timeout-redirect')

                # in case of bogus or expired sessionId, redirect
                if e.errorCode == ServerError.INVALID_SESSION and self.session_invalid_uri:
                    self.server.errorLog.info("Session invalid; redirecting to %s"
                                              % self.session_invalid_uri)
                    new_response_headers = HeaderDict.HeaderDict()
                    self.session.updateHeaders(None, new_response_headers, self.server.errorLog)
                    self.sendRedirect(self.session_invalid_uri, new_response_headers)
                    return True
                elif e.errorCode != ServerError.INVALID_SESSION:
                    from Ft.Server.Server import MessageSource
                    self.server.errorLog.error("Error (%d) during session retrieval: %s" %
                                               (e.errorCode, e.message))

        #If we are here, then there is no login or session (except maybe 403 auth)
        #Try the old-fashioned way
        self.repo = self.getRepository()
        return self.repo is None

    def getHostHeader(self):
        #Apache reverse proxies add X-Forwarded-Host and munge Host
        headers = self.headers_in.terse()
        host_port = headers.get(
            'X-Forwarded-Host',
            headers.get(
                'Host',
                '%s:%s' % (self.server.hostname, self.server.port)
                )
            )
        return host_port

    def sendRedirect(self, response_uri, headers, permanent=False):
        """Generates in the response a Location: header from the given
        response_uri, and sets the response code to 302 or 301, depending
        on whether permanent was 0 or 1. Defaults to 302 (temporary).
        The Location header value must be an absolute URI, optionally
        with a fragment. In the event of a relative URI, the base URI is
        calculated from the request's Host header, if any, or from the
        server's configured hostname and port."""
        # fragment allowed in Location header per RFC 2616 errata
        # (see http://skrb.org/ietf/http_errata.html#location-fragments )
        self.server.errorLog.debug("Redirect to '%s' requested" % response_uri)
        base_uri = 'http://' + self.getHostHeader() + self.path
        response_uri = Uri.Absolutize(response_uri, base_uri)
        self.server.errorLog.info("Preparing response to redirect to %s" % response_uri)
        self.headers_out['Location'] = response_uri
        tempHeaders = headers or HeaderDict.HeaderDict()
        self.headers_out.update(tempHeaders)
        if permanent:
            self.status = Status.HTTP_MOVED_PERMANENTLY
        else:
            self.status = Status.HTTP_MOVED_TEMPORARILY
        return ''


    def load_resource(self, allowRedirect):
        if self.isSoap:
            res = self.repo.fetchResource(self.filename)
            self.stylesheets.append(self.filename)
            return res
        # if the requested resource path is the magic path that indicates that we want
        # to use a query argument value as the resource's path, then try to use it
        #
        # example:  /resourcebyuri?uri=/ftss/data/us-states.xml
        #
        if self.path == self.uri_query_path:
            uri_arg = self.uri_path
            if not uri_arg:
                # Malformed
                msg = _("Requests for %s must have the query argument '%s'" %
                         (self.uri_query_path, self.uri_param))
                self.send_error(Status.HTTP_BAD_REQUEST, error=msg)
                return
            path = uri_arg
        else:
            path = self.filename
        self.server.errorLog.info("Loading Resource: %s\n" % str(path))
        res = self.repo.fetchResource(path)
        #If resource is a container and the resource path in the request did
        #not have a trailing slash, prepare to redirect to the container with the slash
        #NOTE: use unparsed_path because path can contain ;no-traverse
        perm = False
        if self.unparsed_path[-1] != '/' and res.resourceType == ResourceType.CONTAINER:
            allowRedirect = True
            perm = True
            newURI = '%s/' % self.unparsed_path
            # ('', '', '/buyerbase', '', 'foo=bar', '')
            if self.unparsed_params:
                newURI += ';%s' % self.unparsed_params
            if self.unparsed_args:
                newURI += '?%s' % self.unparsed_args
            self.redirects[res.getAbsolutePath()] = newURI
            self.server.errorLog.debug("Container resource requested without trailing '/'. Redirecting.")

        # handle redirects.
        # if there were no query args, allowRedirect will be false.
        if res.getAbsolutePath() in self.redirects and allowRedirect:
            new_response_headers = HeaderDict.HeaderDict()
            if self.session:
                self.session.updateHeaders(self.repo, new_response_headers,
                                               self.server.errorLog)
            self.sendRedirect(self.redirects[res.getAbsolutePath()], new_response_headers, perm)
            return None
        return res

    def load_stylesheets(self, resource):
        """Load all available stylesheets"""
        if not self.stylesheets and not self.disable_default_xslt:
            #See if there is a default
            if resource.resourceType in self.xsltDefaults:
                self.stylesheets.append(
                    self.xsltDefaults[resource.resourceType]
                    )
        stys = []

        for sty in self.stylesheets:
            #Raise to let the exception handling take action
            if resource.__class__ == XmlBody:
                s = self.repo.fetchResource(sty)
                stys.append(s)
            else:
                s = resource.fetchResource(sty)
                stys.append(s)
            self.server.errorLog.info(
                "Loaded Stylesheet: %s\n" % str(s.getAbsolutePath())
                )

        return stys

    def apply_rules(self):
        new_args = {}
        rule_applied, data = (0, None)
        for (pattern, src, xforms, args, pat_str, chain) in self.processing_rules:
            #path = res.getPath().absolutePath
            m = pattern.match(self.path)
            #Hmm.  Is there a case where m is not None *and* m.group() != self.path
            if m and m.group() == self.path:
                self.server.errorLog.info("Applying rule matching pattern '%s'\n" % pat_str)
                try:
                    res = self.repo.fetchResource(self.filename)
                except:
                    res = self.repo
                if src:
                    #Handle regular expression back references in xslt-source
                    #e.g. <Rule pattern="a(b)c" xslt-source="\1"/>
                    #Would produce a source esource of "b" if matched
                    src = REPL_PAT.sub(lambda sm, pm=m: pm.group(int(sm.group(1))), src)
                    res = res.fetchResource(src)
                #Handle regular expression back references in xslt-transform
                xforms = [ REPL_PAT.sub(lambda sm, pm=m: pm.group(int(sm.group(1))), xform) for xform in xforms.split() ]
                stys = [ res.fetchResource(xform) for xform in xforms ]
                if chain:
                    stys = [ (i,) for i in stys ]
                new_args = cgi.parse_qs(args, keep_blank_values=True)
                matched_args = m.groupdict("")
                for k in matched_args:
                    new_args[k] = [matched_args[k]]
                rule_applied, data = (1, (res, stys, new_args))
                break
        return rule_applied, data

    def get_form_encoding(self, form):
        if self.form_encoding: return
        import cgi
        encoding = 'ISO-8859-1'
        fields = cgi.parse_qs(form)
        if self.http_post_encoding_var in fields:
            encoding = fields[self.http_post_encoding_var][0]
        self.form_encoding = encoding


class XmlBody:
    def __init__(self, text, repo):
        self.body = text
        self.repo = repo
        return

    def applyXslt(self, stylesheets, params=None, ignorePis=True,
                  extParams=None, extModules=None):
        """
        applies the specified stylesheets
        (with the specified transformation parameters)
        on the given string
        """
        import Ft.Xml.Xslt.Processor
        from Ft.Server.Common import ResourceTypes, Schema, XmlLib
        from Ft.Server.Server import FtServerServerException, Error
        from Ft.Xml.InputSource import InputSourceFactory

        extModules = extModules or []

        params = params or {}
        extParams = extParams or {}

        #First map the stylesheets
        if not isinstance(stylesheets, (list, tuple)):
            stylesheets = (stylesheets,)
        p = Ft.Xml.Xslt.Processor.Processor()

        p.extensionParams = extParams

        p._repository = self.repo
        mods = p.registerExtensionModules(
            ['Ft.Server.Server.Xslt']+extModules
            )
        p.setDocumentReader(
            FtssInputSource.NonvalidatingReader
            )
        for s in stylesheets:
            if isinstance(s, StringTypes):
                p.appendStylesheet(p.inputSourceFactory.fromUri(s))
            elif hasattr(s,'toStylesheet'):
                #A document reference
                p.appendStylesheetInstance(s.toStylesheet(self.repo.getRoot()))
            elif hasattr(s,'asStylesheet'):
                #A reference to a document in the system
                p.appendStylesheetInstance(s.asStylesheet())

        p.inputSourceFactory = FtssInputSource._FtssInputSourceFactory(
            self.repo._driver)
        rt = p.run(p.inputSourceFactory.fromString(self.body,'ftss:///'), ignorePis=ignorePis,
                         topLevelParams=params)
        if extParams is not None and hasattr(p, 'extensionParams'):
            extParams.update(p.extensionParams)

        imt = p._lastOutputParams.mediaType
        return rt, imt


