"""quixote.util
$HeadURL: svn://svn/repos/trunk/quixote/util.py $
$Id: util.py 21074 2003-03-12 14:44:16Z akuchlin $

Contains various useful functions and classes:

  xmlrpc(request, func) : Processes the body of an XML-RPC request, and calls
                          'func' with the method name and parameters.
  StaticFile            : Wraps a file from a filesystem as a
                          Quixote resource.
  StaticDirectory       : Wraps a directory containing static files as
                          a Quixote namespace. 

StaticFile and StaticDirectory were contributed by Hamish Lawson.
See doc/static-files.txt for examples of their use.
"""

__revision__ = "$Id: util.py 21074 2003-03-12 14:44:16Z akuchlin $"

import sys, xmlrpclib
import os, mimetypes, urllib
from quixote import errors, html
from cStringIO import StringIO

def xmlrpc (request, func):
    """xmlrpc(request:Request, func:callable) : string

    Processes the body of an XML-RPC request, and calls 'func' with
    two arguments, a string containing the method name and a tuple of
    parameters.
    """

    # Get contents of POST body
    method = request.environ['REQUEST_METHOD']
    if method != 'POST':
        request.response.set_status(405, "Only the POST method is accepted")
        return "XML-RPC handlers only accept the POST method."

    length = int(request.environ['CONTENT_LENGTH'])
    data = request.stdin.read(length)

    # Parse arguments
    params, method = xmlrpclib.loads(data)

    try:
        result = func(method, params)
    except xmlrpclib.Fault, exc:
        result = exc
    except:
        # report exception back to client
        result = xmlrpclib.dumps(
            xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value))
            )
    else:
        result = (result,)
        result = xmlrpclib.dumps(result, methodresponse=1)

    request.response.set_header('content-type', 'text/xml')
    return result


class StaticFile:

    """
    Wrapper for a static file on the filesystem.
    """

    def __init__(self, path, follow_symlinks=0,
                 mime_type=None, encoding=None):
        """StaticFile(path:string, follow_symlinks:bool)
        
        Initialize instance with the absolute path to the file.  If
        'follow_symlinks' is true, symbolic links will be followed.
        'mime_type' specifies the MIME type, and 'encoding' the
        encoding; if omitted, the MIME type will be guessed,
        defaulting to text/plain.        
        """

        # Check that the supplied path is absolute and (if a symbolic link) may
        # be followed 
        self.path = path
        if not os.path.isabs(path):
            raise ValueError, "Path %r is not absolute" % path
        if os.path.islink(path) and not follow_symlinks:
            raise errors.TraversalError(private_msg="Path %r is a symlink"
                                        % path)

        # Decide the Content-Type of the file
        guess_mime, guess_enc = mimetypes.guess_type(os.path.basename(path),
                                                     strict=0)
        self.mime_type = mime_type or guess_mime or 'text/plain'
        self.encoding = encoding or guess_enc or None

    def __call__(self, request):
        # Set the Content-Type for the response and return the file's contents.
        request.response.set_content_type(self.mime_type)
        if self.encoding:
            request.response.set_header("Content-Encoding", self.encoding)
        fsfile = open(self.path, 'rb')
        contents = fsfile.read()
        fsfile.close()
        return contents


class StaticDirectory:

    """
    Wrap a filesystem directory containing static files as a Quixote namespace.
    """

    _q_exports = []

    def __init__(self, path, use_cache=0, list_directory=0, follow_symlinks=0):
        """StaticDirectory(path:string, use_cache:bool,
                             list_directory:bool, follow_symlinks:bool)
        
        Initialize instance with the absolute path to the file.
        If 'use_cache' is true, StaticFile instances will be cached in memory.
        If 'list_directory' is true, users can request a directory listing.
        If 'follow_symlinks' is true, symbolic links will be followed.
        """

        # Check that the supplied path is absolute
        self.path = path
        if not os.path.isabs(path):
            raise ValueError, "Path %r is not absolute" % path

        self.use_cache = use_cache
        self.cache = {}
        self.list_directory = list_directory
        self.follow_symlinks = follow_symlinks

    def _q_index(self, request):
        """
        If directory listings are allowed, generate a simple HTML
        listing of the directory's contents with each item hyperlinked;
        if the item is a subdirectory, place a '/' after it. If not allowed,
        return a page to that effect.
        """
        out = StringIO()
        if self.list_directory:
            template = html.htmltext('<a href="%s">%s</a>%s')
            print >>out, (html.htmltext("<h1>%s</h1>")
                          % request.environ['REQUEST_URI'])
            print >>out, "<pre>"
            print >>out, template % ('..', '..', '')
            files = os.listdir(self.path)
            files.sort()
            for filename in files:
                filepath = os.path.join(self.path, filename)
                marker = os.path.isdir(filepath) and "/" or ""
                print >>out, \
                        template % (urllib.quote(filename), filename, marker)
            print >>out, "</pre>"
        else:
            print >>out, "<h1>Directory listing denied</h1>"
            print >>out, \
                "<p>This directory does not allow its contents to be listed.</p>"
        return out.getvalue()

    def _q_getname(self, request, name):
        """
        Get a file from the filesystem directory and return the StaticFile
        or StaticDirectory wrapper of it; use caching if that is in use.
        """
        if name in ('.', '..'):
            raise errors.TraversalError(private_msg="Attempt to use '.', '..'")
        if self.cache.has_key(name):
            # Get item from cache
            item = self.cache[name]
        else:
            # Get item from filesystem; cache it if caching is in use.
            item_filepath = os.path.join(self.path, name)
            while os.path.islink(item_filepath):
                if not self.follow_symlinks:
                    raise errors.TraversalError
                else:
                    dest = os.readlink(item_filepath)
                    item_filepath = os.path.join(self.path, dest)

            if os.path.isdir(item_filepath):
                item = StaticDirectory(item_filepath, self.use_cache,
                        self.list_directory, self.follow_symlinks)
            elif os.path.isfile(item_filepath):
                item = StaticFile(item_filepath, self.follow_symlinks)
            else:
                raise errors.TraversalError
            if self.use_cache:
                self.cache[name] = item
        if isinstance(item, StaticDirectory):
            return item
        else:
            return item(request)


