"""quixote.publish

Logic for publishing modules and objects on the Web.
"""

__revision__ = "$Id: publish.py,v 1.35 2000/08/10 15:37:00 gward Exp $"

import sys, traceback, os, string, cStringIO
import time, types

from quixote import init, session, errors
from quixote.util import fcgi
from quixote.zope import HTTPRequest, HTTPResponse

import config

# Dictionary used to store the time when a module was imported
time_imported = {}


def get_module(name):
    """Get a module object by name."""
    __import__(name)
    module = sys.modules[name]
    return module
    
def _get_module_filename(module):
    filename = module.__file__
    if os.path.isdir( filename ):
        filename = os.path.join( filename, '__init__.py')
    elif filename[ -4: ] == '.pyc': filename = filename[:-1]

    return filename

def _traverse (path, request, response):
    """_traverse(path:list of strings,
                 request, response : various objects) ->
          (script_name: string, path_info : string, object : any)

    Perform traversal based on the provided path, starting at the root
    object.  It returns the script name and path info values for
    the arrived-at object, along with the object itself.

    It's expected that the final object is something callable like a
    function or a method; intermediate objects along the way will
    usually be packages or modules.

    To prevent crackers from writing URLs that traverse private
    objects, every package, module, or object along the way must have
    an __exports__ attribute containing a list of publicly visible
    names.  Not having an __exports__ attribute is an error, though
    having __exports__ be an empty list is OK (though useless).  If a
    component of the path isn't in __exports__, that also produces an
    error.
    """

    # Traverse starting at the root
    object = get_module(config.ROOT_PACKAGE)

    # Loop over the components of the path, checking at every step
    # 1) whether __exports__ is present, 2) whether the path component
    # is listed in __exports__

    index = 0
    while index < len(path):
        component = path[index]
        if not hasattr(object, '__exports__'):
            # XXX what exception to raise here?
            raise ValueError, "Object %s has no __exports__" % repr(object)

        if component not in object.__exports__:
            raise ValueError, ("Attribute %s of %s "
                               "not in __exports__: %s" %
                               (repr(component), object, object.__exports__) )

        # Call __access__ function or method, if it's present
        if hasattr(object, '__access__'):
            access_func = getattr(object, '__access__')
            reasons = errors.AccessError()
            access_func( reasons, request, response )
            if reasons.has_reasons():
                raise reasons
            
        # Get next object, which should be an attribute of the current object
        if type(object) is not types.ModuleType:
            object = getattr(object, component)

        elif (hasattr(object, component) and
                type(getattr(object, component)) is not types.ModuleType):
            # module has attribute and attribute is not a module
            object = getattr(object, component)

        else:
            # get next module
            mod_name = object.__name__ + '.' + component
            object = get_module(mod_name)

        # Bail out of the 'while' loop early once we reach something
        # that isn't a namespace, such as a method or function.
        if (type(object) == types.FunctionType or
            type(object) == types.MethodType):
            break

        index = index + 1

    # Determine the script name and path info to be returned.
    # If path_info is not the empty string, it must begin with a
    # trailing slash, to guarantee that
    # "http://$SERVER_NAME:$SERVER_PORT$SCRIPT_NAME$PATH_INFO" will
    # always be an accessible URL that points to the current script.
    # This invariant comes from the cgi_path.html file in the Apache docs.
    script_name = config.URL_PREFIX + '/' + string.join(path[:index+1], '/')
    path_info =   string.join(path[index+1:], '/')
    if path_info != "": path_info = '/' + path_info 
    
    return script_name, path_info, object


def traverse_url (request, response):
    """traverse_url() -> (script_name, path_info, object)

    Reads the PATH_INFO environment variable and traverses the path
    found there.

    If the object ends up being a module, it's checked for a function
    named 'index', and that module will be returned instead.
    """

    # Get path and split it apart; /foo/bar/baz -> ['foo', 'bar', 'baz']
    original_path = os.environ.get('PATH_INFO', '')
    path = string.split(original_path, '/')
    path = filter(None, path)              # Drop empty strings
    
    script_name, path_info, object = _traverse(path, request, response)

    if type(object) == types.ModuleType:
        # It's a module, which isn't callable.  First, make sure URL has
        # trailing slash
        if original_path[-1] != '/':
            # repair path and redirect
            path = "%s/%s/" % (config.URL_PREFIX, string.join(path, '/'))
            def redirect_slash(request, response, path=path):
                return response.redirect(path)
            return '', '', redirect_slash
        
        # Check if the module has a function named 'index', and use that
        # function instead
        if (hasattr(object, 'index') and 'index' in object.__exports__):
            object = object.index
        else:
            raise ValueError, "Module %s has no index" % object.__name__
            
    return script_name, path_info, object

def send_traceback (traceback_msg, request, response):
    """send_traceback(traceback_msg : string,
                      request : HTTPRequest,
                      response : HTTPResponse)

    If Quixote has been configured to do so, send an e-mail notifying
    someone of a traceback. 
    """
    
    if config.ERROR_EMAIL is None: return
    
    pipe = os.popen('/usr/lib/sendmail -f "Quixote" %s' % config.ERROR_EMAIL, 'w')
    pipe.write('Subject: Traceback\n')
    pipe.write('\n')

    pipe.write('Traceback\n=========\n')
    traceback.print_exc( file = pipe)

    if request is None:
        pipe.write('\nRequest is None\n')
    else:
        pipe.write('\nRequest\n=======\n')
        pipe.write( request.dump() )

    if response is None:
        pipe.write('\nResponse is None\n')
    else:
        pipe.write('\nResponse\n=======\n')
        pipe.write( str(response) )

    pipe.close()

exit_now = 0
    
def call (stdin, stdout, stderr, *args):
    global exit_now

    """call() -> None

    The master function that does all the work for a single request.
    call() uses traverse_url() to get a callable object, creates the
    'request' and 'response' objects, and calls the object.  If it
    raises an exception, the traceback is logged and e-mailed.

    Any extra arguments will be passed to the callable.
    """

    output = request = response = None
    sess_coll = session.get_session_collection()
    
    # Build request, response objects
    response = HTTPResponse.HTTPResponse( server_name = os.environ['SERVER_NAME'] )
    request = HTTPRequest.HTTPRequest( stdin, os.environ, response )
    request.processInputs()

    # Get the session object and stick it onto the request
    request.session = session.get_session (request, response)

    try:
        # Traverse package to a (hopefully-) callable object
        (os.environ['SCRIPT_NAME'],
         os.environ['PATH_INFO'],
         callable) = traverse_url(request, response)

        # Call object
        try:
            args = (request, response) + args
            output = apply(callable, args)
        except SystemExit:
            output = "Server restarted"
            exit_now = 1

        if output is None:
            raise RuntimeError, 'Callable %s returned None' % repr(callable)

    except errors.IgnoredException:
        # The callable ran OK, so return its output to the client
        sess_coll.commit()               # Commit any changes to the session

        location = response.headers['location']
        output = '<a href="%s">Follow this link</a>' % location
        response.setBody ( output )
        
        # Output results from Response object
        stdout.write( str( response ) )
        
    except:
        traceback_file = cStringIO.StringIO()
        traceback.print_exc( file = traceback_file)

        if config.DISPLAY_EXCEPTIONS:
            stdout.write('Content-type: text/plain\n\n')
            stdout.write( traceback_file.getvalue() )

        tb = traceback_file.getvalue()
        if config.ERROR_LOG is None:
            sys.stderr.write( tb )
        else:
            try:
                errlog = open(config.ERROR_LOG, 'a')
            except IOError:
                sys.stderr.write("Cannot write traceback to error log %s\n"
                                 % repr(config.ERROR_LOG) )
            else:
                errlog.write(tb)
                errlog.close()
            
        send_traceback( tb, request, response )

        # Abort any changes to the session
        sess_coll.abort()

    else:
        # The callable ran OK, so return its output to the client
        sess_coll.commit()               # Commit any changes to the session
        response.setBody ( output )

        # Output results from Response object
        stdout.write( str( response ) )


def call_fcgi ():
    """call_fcgi() -> None

    Entry point from CGI scripts; it will repeatedly do the call()
    function until there are no more FCGI requests.  This function can
    also be called in regular CGI scripts, in which case call() is
    called exactly once.
    """
    while fcgi.isFCGI() and not exit_now:
        f = fcgi.FCGI()
        os.environ = f.env
        try:
            call(f.inp, f.out, f.err)
        except errors.AccessError, reasons:
            os.environ['PATH_INFO'] = config.ACCESS_VIOLATION_PAGE
            call(f.inp, f.out, f.err, reasons)
        
        f.Finish()
        if config.RUN_ONCE: break
