# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/
# Original PyFileServer (c) 2005 Ho Chun Wei.
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
"""
WSGI container, that handles the HTTP requests. This object is passed to the 
WSGI server and represents our WsgiDAV application to the outside. 

On init:

    Use the configuration dictionary to initialize lock manager, property manager,
    domain controller. 

    Create a dictionary of share-to-provider mappings.         

    Initialize middleware objects and RequestResolver and setup the WSGI 
    application stack.
      
For every request:

    Find the registered DAV provider for the current request.   
    
    Add or modify info in the WSGI ``environ``:
    
        environ["SCRIPT_NAME"]
            Mount-point of the current share.            
        environ["PATH_INFO"]
            Resource path, relative to the mount path.
        environ["wsgidav.provider"]
            DAVProvider object that is registered for handling the current 
            request. 
        environ["wsgidav.config"]
            Configuration dictionary.
        environ["wsgidav.verbose"]
            Debug level [0-3].

    Log the HTTP request, then pass the request to the first middleware.

    Note: The OPTIONS method for the '*' path is handled directly.

See `Developers info`_ for more information about the WsgiDAV architecture.

.. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html  
"""
from fs_dav_provider import FilesystemProvider
from wsgidav.dir_browser import WsgiDavDirBrowser
from wsgidav.dav_provider import DAVProvider
from wsgidav.lock_storage import LockStorageDict
import time
import sys
import threading
import urllib
import util
from error_printer import ErrorPrinter
from debug_filter import WsgiDavDebugFilter
from http_authenticator import HTTPAuthenticator
from request_resolver import RequestResolver
from domain_controller import WsgiDAVDomainController
from property_manager import PropertyManager
from lock_manager import LockManager
#from wsgidav.version import __version__

__docformat__ = "reStructuredText"


# Use these settings, if config file does not define them (or is totally missing)
DEFAULT_CONFIG = {
    "mount_path": None,  # Application root, e.g. <mount_path>/<share_name>/<res_path>               
    "provider_mapping": {},
    "host": "localhost",
    "port": 8080, 
    "ext_servers": [
#                   "paste", 
#                   "cherrypy",
#                   "wsgiref",
                   "cherrypy-bundled",
                   "wsgidav",
                   ],

    "enable_loggers": [
                      ],

    "propsmanager": None,  # True: use property_manager.PropertyManager                  
    "locksmanager": True,  # True: use lock_manager.LockManager    
    
    # HTTP Authentication Options
    "user_mapping": {},       # dictionary of dictionaries 
    "domaincontroller": None, # None: domain_controller.WsgiDAVDomainController(user_mapping)
    "acceptbasic": True,      # Allow basic authentication, True or False
    "acceptdigest": True,     # Allow digest authentication, True or False
    "defaultdigest": True,    # True (default digest) or False (default basic)
    
    # Verbose Output
    "verbose": 2,        # 0 - no output (excepting application exceptions)         
                         # 1 - show single line request summaries (for HTTP logging)
                         # 2 - show additional events
                         # 3 - show full request/response header info (HTTP Logging)
                         #     request body and GET response bodies not shown
    
    "dir_browser": {
        "enable": True,          # Render HTML listing for GET requests on collections
        "response_trailer": "",  # Raw HTML code, appended as footer
        "davmount": False,       # Send <dm:mount> response if request URL contains '?davmount'
        "msmount": False,        # Add an 'open as webfolder' link (requires Windows)
    }
}




def _checkConfig(config):
    mandatoryFields = ["provider_mapping",
                       ]
    for field in mandatoryFields:
        if not field in config:
            raise ValueError("Invalid configuration: missing required field '%s'" % field)




#===============================================================================
# WsgiDAVApp
#===============================================================================
class WsgiDAVApp(object):

    def __init__(self, config):
        self.config = config

        util.initLogging(config["verbose"],
                         config.get("log_path", ""),
                         config.get("enable_loggers", []))
        
        util.log("Default encoding: %s (file system: %s)" % (sys.getdefaultencoding(), sys.getfilesystemencoding()))
        
        # Evaluate configuration and set defaults
        _checkConfig(config)
        provider_mapping = self.config["provider_mapping"]
#        response_trailer = config.get("response_trailer", "")
        self._verbose = config.get("verbose", 2)

        lockStorage = config.get("locksmanager") 
        if lockStorage is True:
            lockStorage = LockStorageDict()
            
        if not lockStorage:
            locksManager = None
        else:
            locksManager = LockManager(lockStorage)

        propsManager = config.get("propsmanager")     
        if not propsManager:
            # Normalize False, 0 to None
            propsManager = None
        elif propsManager is True:
            propsManager = PropertyManager()     

        mount_path = config.get("mount_path")
         
        user_mapping = self.config.get("user_mapping", {})
        domainController = config.get("domaincontroller") or WsgiDAVDomainController(user_mapping)
        isDefaultDC = isinstance(domainController, WsgiDAVDomainController)

        # authentication fields
        authacceptbasic = config.get("acceptbasic", True)
        authacceptdigest = config.get("acceptdigest", True)
        authdefaultdigest = config.get("defaultdigest", True)
        
        # Check configuration for NTDomainController
        # We don't use 'isinstance', because include would fail on non-windows boxes.
        wdcName = "NTDomainController"
        if domainController.__class__.__name__ == wdcName:
            if authacceptdigest or authdefaultdigest or not authacceptbasic:
                util.warn("WARNING: %s requires basic authentication.\n\tSet acceptbasic=True, acceptdigest=False, defaultdigest=False" % wdcName)
                
        # Instantiate DAV resource provider objects for every share
        self.providerMap = {}
        for (share, provider) in provider_mapping.items():
            # Make sure share starts with, or is, '/' 
            share = "/" + share.strip("/")

            # We allow a simple string as 'provider'. In this case we interpret 
            # it as a file system root folder that is published. 
            if isinstance(provider, basestring):
                provider = FilesystemProvider(provider)

            assert isinstance(provider, DAVProvider)

            provider.setSharePath(share)
            if mount_path:
                provider.setMountPath(mount_path)
            
            # TODO: someday we may want to configure different lock/prop managers per provider
            provider.setLockManager(locksManager)
            provider.setPropManager(propsManager)
            
            self.providerMap[share] = provider
            

        if self._verbose >= 2:
            print "Using lock manager: %r" % locksManager
            print "Using property manager: %r" % propsManager
            print "Using domain controller: %s" % domainController
            print "Registered DAV providers:"
            for share, provider in self.providerMap.items():
                hint = ""
                if isDefaultDC and not user_mapping.get(share):
                    hint = " (anonymous)"
                print "  Share '%s': %s%s" % (share, provider, hint)

        # If the default DC is used, emit a warning for anonymous realms
        if isDefaultDC and self._verbose >= 1:
            for share in self.providerMap:
                if not user_mapping.get(share):
                    # TODO: we should only warn here, if --no-auth is not given
                    print "WARNING: share '%s' will allow anonymous access." % share

        # Define WSGI application stack
        application = RequestResolver()
        
        if config.get("dir_browser") and config["dir_browser"].get("enable", True):
            application = config["dir_browser"].get("app_class", WsgiDavDirBrowser)(application)

        application = HTTPAuthenticator(application, 
                                        domainController, 
                                        authacceptbasic, 
                                        authacceptdigest, 
                                        authdefaultdigest)      
        application = ErrorPrinter(application, catchall=True)

        application = WsgiDavDebugFilter(application, config)
        
        self._application = application


    def __call__(self, environ, start_response):

#        util.log("SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))
        
        # We unquote PATH_INFO here, although this should already be done by
        # the server.
        path = urllib.unquote(environ["PATH_INFO"])
        # issue 22: Pylons sends root as u'/' 
        if isinstance(path, unicode):
            util.log("Got unicode PATH_INFO: %r" % path)
            path = path.encode("utf8")

        # Always adding these values to environ:
        environ["wsgidav.config"] = self.config
        environ["wsgidav.provider"] = None
        environ["wsgidav.verbose"] = self._verbose

        ## Find DAV provider that matches the share

        # sorting share list by reverse length
        shareList = self.providerMap.keys()
        shareList.sort(key=len, reverse=True)

        share = None 
        for r in shareList:
            # @@: Case sensitivity should be an option of some sort here; 
            #     os.path.normpath might give the preferred case for a filename.
            if r == "/":
                share = r
                break
            elif path.upper() == r.upper() or path.upper().startswith(r.upper()+"/"):
                share = r
                break
        
        provider = self.providerMap.get(share)
        
        # Note: we call the next app, even if provider is None, because OPTIONS 
        #       must still be handled.
        #       All other requests will result in '404 Not Found'  
        environ["wsgidav.provider"] = provider

        # TODO: test with multi-level realms: 'aa/bb'
        # TODO: test security: url contains '..'
        
        # Transform SCRIPT_NAME and PATH_INFO
        # (Since path and share are unquoted, this also fixes quoted values.)
        if share == "/" or not share:
            environ["PATH_INFO"] = path
        else:
            environ["SCRIPT_NAME"] += share
            environ["PATH_INFO"] = path[len(share):]
#        util.log("--> SCRIPT_NAME='%s', PATH_INFO='%s'" % (environ.get("SCRIPT_NAME"), environ.get("PATH_INFO")))

        assert isinstance(path, str)
        # See http://mail.python.org/pipermail/web-sig/2007-January/002475.html
        # for some clarification about SCRIPT_NAME/PATH_INFO format
        # SCRIPT_NAME starts with '/' or is empty
        assert environ["SCRIPT_NAME"] == "" or environ["SCRIPT_NAME"].startswith("/")
        # SCRIPT_NAME must not have a trailing '/'
        assert environ["SCRIPT_NAME"] in ("", "/") or not environ["SCRIPT_NAME"].endswith("/")
        # PATH_INFO starts with '/'
        assert environ["PATH_INFO"] == "" or environ["PATH_INFO"].startswith("/")

        start_time = time.time()
        def _start_response_wrapper(status, response_headers, exc_info=None):
            # Postprocess response headers
            headerDict = {}
            for header, value in response_headers:
                if header.lower() in headerDict:
                    util.warn("Duplicate header in response: %s" % header)
                headerDict[header.lower()] = value

            # Check if we should close the connection after this request. 
            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
            forceCloseConnection = False
            currentContentLength = headerDict.get("content-length") 
            statusCode = int(status.split(" ", 1)[0]) 
            contentLengthRequired = (environ["REQUEST_METHOD"] != "HEAD" 
                                     and statusCode >= 200
                                     and not statusCode in (204, 304))  
#            print environ["REQUEST_METHOD"], statusCode, contentLengthRequired
            if contentLengthRequired and currentContentLength in (None, ""):
                # A typical case: a GET request on a virtual resource, for which  
                # the provider doesn't know the length 
                util.warn("Missing required Content-Length header in %s-response: closing connection" % statusCode)
                forceCloseConnection = True
            elif not type(currentContentLength) is str:
                util.warn("Invalid Content-Length header in response (%r): closing connection" % headerDict.get("content-length"))
                forceCloseConnection = True
            
            # HOTFIX for Vista and Windows 7 (issue 13, issue 23)
            # It seems that we must read *all* of the request body, otherwise
            # clients may miss the response.
            # For example Vista MiniRedir didn't understand a 401 response, 
            # when trying an anonymous PUT of big files. As a consequence, it
            # doesn't retry with credentials and the file copy fails. 
            # (XP is fine however).
            util.readAndDiscardInput(environ)

            # Make sure the socket is not reused, unless we are 100% sure all 
            # current input was consumed
            if(util.getContentLength(environ) != 0 
               and not environ.get("wsgidav.all_input_read")):
                util.warn("Input stream not completely consumed: closing connection")
                forceCloseConnection = True
                
            if forceCloseConnection and headerDict.get("connection") != "close":    
                util.warn("Adding 'Connection: close' header")
                response_headers.append(("Connection", "close"))
            
            # Log request
            if self._verbose >= 1:
                userInfo = environ.get("http_authenticator.username")
                if not userInfo:
                    userInfo = "(anonymous)"
                threadInfo = ""
                if self._verbose >= 1:
                    threadInfo = "<%s> " % threading._get_ident()
                extra = []
                if "HTTP_DESTINATION" in environ:
                    extra.append('dest="%s"' % environ.get("HTTP_DESTINATION"))
                if environ.get("CONTENT_LENGTH", "") != "":
                    extra.append("length=%s" % environ.get("CONTENT_LENGTH"))
                if "HTTP_DEPTH" in environ:
                    extra.append("depth=%s" % environ.get("HTTP_DEPTH"))
                if "HTTP_RANGE" in environ:
                    extra.append("range=%s" % environ.get("HTTP_RANGE"))
                if "HTTP_OVERWRITE" in environ:
                    extra.append("overwrite=%s" % environ.get("HTTP_OVERWRITE"))
                if self._verbose >= 1 and "HTTP_EXPECT" in environ:
                    extra.append('expect="%s"' % environ.get("HTTP_EXPECT"))
                if self._verbose >= 2 and "HTTP_CONNECTION" in environ:
                    extra.append('connection="%s"' % environ.get("HTTP_CONNECTION"))
                if self._verbose >= 2 and "HTTP_USER_AGENT" in environ:
                    extra.append('agent="%s"' % environ.get("HTTP_USER_AGENT"))
                if self._verbose >= 2 and "HTTP_TRANSFER_ENCODING" in environ:
                    extra.append('transfer-enc=%s' % environ.get("HTTP_TRANSFER_ENCODING"))
                if self._verbose >= 1:
                    extra.append('elap=%.3fsec' % (time.time() - start_time))
                extra = ", ".join(extra)
                        
                util.log('%s - %s - "%s" %s -> %s' % (
                        environ.get("REMOTE_ADDR",""),
                        userInfo,
                        environ.get("REQUEST_METHOD") + " " + environ.get("PATH_INFO", ""),
                        extra, 
                        status
                        ))

            return start_response(status, response_headers, exc_info)
            
        # Call next middleware
        for v in self._application(environ, _start_response_wrapper):
            yield v
        return
