########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Xslt/Repository.py,v 1.45 2005/04/06 23:05:46 jkloth Exp $
"""
XSLT and XPath extensions supporting the 4SS repository API

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

import sha, cStringIO

from Ft.Lib import boolean
from Ft.Xml import Domlette
from Ft.Xml.XPath import Conversions
from Ft.Xml.Xslt import XsltElement, ContentInfo, AttributeInfo
from Ft.Server import FTSERVER_NAMESPACE, RESERVED_NAMESPACE
from Ft.Server.Server import FtServerServerException,Error

import FtssXsltBase
from Ns import SCORE_NS


#def RepoUserName(context):
#    repo = context.processor._repository
#    return repo.getUserName() or ''


class TextSearchRepositoryElement(XsltElement):
    """
    Invoke the full-text search to find text in the repository
    (Requires SWISH-E as supporting software)
    """

    content = ContentInfo.Empty

    legalAttrs = {
        'query' : AttributeInfo.StringAvt(
            required=1, description='The string to search for'),
        }

    def instantiate(self, context, processor):
        context.setProcessState(self)

        queryString = Conversions.StringValue(self._query.evaluate(context))

        repo = context.processor._repository
        results = repo.searchDocuments(queryString)

        processor.writers[-1].startElement('Results')
        for result in results:
            processor.writers[-1].startElement('Match')
            processor.writers[-1].text(result)
            processor.writers[-1].endElement('Match')

        processor.writers[-1].endElement('Results')
        return


class TextReindexRepositoryElement(XsltElement):
    """
    Trigger a re-indexing for full-text in the repository
    (Requires SWISH-E as supporting software)
    """

    content = ContentInfo.Empty
    legalAttrs = {}

    def instantiate(self, context, processor):
        repo = context.processor._repository
        repo.reindex()
        return


class LoginElement(XsltElement):
    """
    Log in a user and create a new repository session accordingly,
    establishing a session for the logged in user with implicit log in on
    all subsequent requests from that agent until timeout.  Sessions also
    support the management of persistent state data.
    When the repo session eventually times out, the user is redirected
    to timeout-redirect on the next request.
    """
    content = ContentInfo.Empty
    legalAttrs = {
        'user' : AttributeInfo.StringAvt(
            required=1, description='The 4Suite user name'),
        'password' : AttributeInfo.StringAvt(
            required=1, description='The 4Suite user password'),
        'ttl' : AttributeInfo.NumberAvt(
            default=0,
            description=('The time to live (in seconds) for the session.'
                         ' If omitted, the driver default is used (usually'
                         ' 15 minutes)')),
        'success-redirect' : AttributeInfo.StringAvt(
            description=('A URI to redirect to if the login succeeds.'
                         ' Defaults to the current page.')),
        'failure-redirect' : AttributeInfo.StringAvt(
            description=('A URI to redirect to if the login fails.'
                         ' Defaults to the current page.')),
        'timeout-redirect' : AttributeInfo.StringAvt(
            description=('A URI to redirect to for requests after the'
                         ' session eventually times out. Defaults to the'
                         ' current page.')),
        }

    def instantiate(self, context, processor):
        from Ft.Server.Server.Http import Session
        from Ft.Server import RESERVED_NAMESPACE
        repo = context.processor._repository
        user = self._user.evaluate(context)
        password = sha.new(self._password.evaluate(context)).hexdigest()
        #print "Session-based login requested via fcore:login for user %s" % user
        processor.extensionParams[(RESERVED_NAMESPACE, 'login')] = (
            user, password,
            self._ttl.evaluate(context),
            self._success_redirect.evaluate(context),
            self._failure_redirect.evaluate(context),
            self._timeout_redirect.evaluate(context),
            )
        #SessionInvalidate(context)
        return


class CreateSessionElement(XsltElement):
    """
    Create a new repository session.  This allows implicit log in on all requests, and the management of persistent state data.
    """
    content = ContentInfo.Empty
    legalAttrs = {
        'key' : AttributeInfo.StringAvt(
            description=('The session key to use.  If omitted, a session'
                         ' key is generated for you.')),
        'ttl' : AttributeInfo.NumberAvt(
            default=0,
            description=('The time to live for the session.  If omitted,'
                         ' the driver default is used (usually 15 minutes)')),
        #'id' : AttributeInfo.StringAvt(description='The session ID to attach'),
        }

    def instantiate(self, context, processor):
        from Ft.Server.Server.Http import Session
        from Ft.Server import RESERVED_NAMESPACE
        repo = context.processor._repository
        id = repo.createSession(self._key.evaluate(context), self._ttl.evaluate(context))
        handler = processor.extensionParams[(RESERVED_NAMESPACE, 'handler')]
        processor.extensionParams[(RESERVED_NAMESPACE, 'sessionId')] = id
        handler.session = Session.Create(id, handler.session_method)
        processor.writers[-1].text(unicode(id))
        print "Session set to", id
        return


class _XsltSessionData:
    def __init__(self, type, value, baseURI=None):
        self.type = type
        self.value = value
        self.baseURI = baseURI
        return

class SessionDataElement(XsltElement):
    """
    Sets a key/value pair in the data for the current session data,
    for persistence in the user agent
    If the "value" attribute is used, the resulting string is the session
    entry value.  If this attribute is omitted, a node set is the resulting
    value, whose sole entry is the root node of the result tree fragment
    from instantiating the template that makes up the content of this element.
    """
    content = ContentInfo.Template
    legalAttrs = {
        'key' : AttributeInfo.StringAvt(
            required=1,
            description='The session key to use'),
        'value' : AttributeInfo.StringAvt(
            description='A string value for the session datum')
        }

    def instantiate(self, context, processor):
        key = self._key.evaluate(context)
        value = self._value.evaluate(context)
        repo = processor._repository
        if repo.hasSession():
            if value:
                value = _XsltSessionData('string', value)
            else:
                processor.pushResultTree(self.baseUri)
                # Add a wrapper element to allow the tree to be parsed later
                processor.writers[-1].startElement("session-data-wrapper")
                try:
                    self.processChildren(context, processor)
                finally:
                    processor.writers[-1].endElement("session-data-wrapper")
                    result = processor.popResult()

                buf = cStringIO.StringIO()
                Domlette.Print(result, stream=buf)
                value = _XsltSessionData('nodeset', buf.getvalue(),
                                         self.baseUri)

            repo.setSessionData(key, value)
        return


def HasSession(context):
    """
    See if this connection is in a session
    """
    repo = context.processor._repository
    if repo.hasSession():
        return boolean.true
    return boolean.false


def SessionInvalidate(context):
    """
    Destroy the current session
    """
    print "Invalidating session at user request"
    try:
        repo = context.processor._repository
        if repo.hasSession():
            repo.invalidateSession()
            return boolean.true
        else:
            return boolean.false
    except:
        return boolean.false


def SessionData(context, key):
    """
    Session data is stored under a session ID (a UUID string) and takes the
    form of zero or more key-value pairs. Each key is a unique string, and each
    value is either a string or a node-set.

    This XSLT extension function coerces the given key to its string-value
    and returns from the current session the node-set that is associated with
    that key. If the key does not exist in the current session, or if there is
    no current session, then an empty node-set is returned. There is no way to
    distinguish between an empty node-set as an indication of such an error and
    an empty node-set as the legitimate value for the given key.
    """

    key = Conversions.StringValue(key)
    repo = context.processor._repository
    rv = []
    if repo.hasSession():
        try:
            data = repo.getSessionData(key)
            if data is not None:
                print "Session data found for key '%s'" % key
                if isinstance(data, _XsltSessionData):
                    # Restore wrapped data
                    if data.type == 'string':
                        rv = data.value
                    elif data.type == 'nodeset':
                        doc = context.processor._docReader.parseString(
                                data.value, data.baseURI,
                                driver=repo._driver)
                        # Remove the wrapper element
                        wrapper = doc.documentElement
                        while wrapper.firstChild:
                            doc.appendChild(wrapper.firstChild)
                        doc.removeChild(wrapper)
                        # Return as a nodeset
                        rv = [doc]
                    else:
                        print "Malformed session data instance: %r" % data
                else:
                    print "Non-XSLT created session data!"
                    rv = data
            else:
                print "There is a current Session, but it has no data for key '%s'." % key
        except:
            print "There was an error getting the Session data."
    else:
        print "There is no current Session."
    return rv


def DeleteSessionData(context, key):
    """
    Delete one of the key/value pairs stored in the session data
    """
    key = Conversions.StringValue(key)
    try:
        repo = context.processor._repository
        if not repo.hasSession():
            return ''
        print "Remove Session Data for %s" % (key)
        repo.deleteSessionData(key)
        return ''
    except:
        return ''


def GetAllDocumentDefinitionPaths(context, path=None):
    """
    Get the resource (path) given a user or group name
    """
    path = Conversions.StringValue(path)
    base = FtssXsltBase.FetchBaseObject(context.processor,path)
    root = base.getRoot()

    doc = context.node.rootNode
    return map(doc.createTextNode, root.getAllDocumentDefinitionPaths())


def GetUserOrGroupByName(context, name):
    """
    Given a user name, return the user resource path
    """
    repo = context.processor._repository
    name = Conversions.StringValue(name)
    try:
        user_or_group = repo.fetchUserOrGroupByName(name)
    except FtServerServerException, e:
        if e.errorCode == Error.PERMISSION_DENIED:
            return "";
        raise
    if user_or_group is None:
        return ''
    else:
        return user_or_group.getAbsolutePath()


def HashString(context, rawString):
    """
    Return a hash for a string
    DEPRECATED - use f:sha-hash()
    """
    print 'Notice: please use extension function "f:sha-hash()"' \
          ' rather than the deprecated "fcore:hash-string()"'
    rstring = Conversions.StringValue(rawString)
    return sha.new(rstring).hexdigest()


class RollbackElement(XsltElement):
    """
    Abort the current transaction (governing the current XSLT operation)
    which will undo all repository operations
    """

    content = ContentInfo.Empty

    legalAttrs = {}

    def instantiate(self, context, processor):
        context.setProcessState(self)
        processor.extensionParams[(RESERVED_NAMESPACE, 'rollback')] = 1
        return


def GetCurrentUser(context):
    """
    Get the resource (path) for the current user
    """
    repo = context.processor._repository
    user = repo.getCurrentUser()
    if user is None:
        return ''
    else:
        return user.getAbsolutePath()


def _GetUserName(context):
    print 'Notice: please use extension function "fcore:get-current-username()"' \
          ' rather than the deprecated "fcore:get-user-name()"'
    return GetCurrentUserName(context)


def GetCurrentUserName(context):
    """
    Get the name (not the resource) of the current user
    """
    repo = context.processor._repository
    user = repo.getCurrentUser()
    if user is None:
        #return "Anonymous"  # should be 'world'?
        from Ft.Server.Common.AclConstants import ANONYMOUS_USER_NAME
        return ANONYMOUS_USER_NAME
    else:
        return user.getUsername()


def RepoPathToUriFunc(context, path):
    """
    Convert a full repository URI e.g. ("ftss:///spam/eggs") to a
    repository path (e.g. "/spam/eggs")
    """
    path = Conversions.StringValue(path)
    from Ft.Server.Server.Drivers.PathImp import RepoPathToUri
    return RepoPathToUri(path)


def UriToRepoPath(context, uri):
    """
    Convert a repository path (e.g. "/spam/eggs") to a proper repository URI
    e.g. ("ftss:///spam/eggs")
    """
    uri = Conversions.StringValue(uri)
    if uri.startswith('ftss://'):
        return uri[7:]
    else:
        return uri


def NormalizeUri(context, uri, base=None):
    """
    Resolve a relative URI against a given base, in effect just
    turning it into a proper repository URI (ftss://...) if no base
    is given for a relative path
    """
    from Ft.Lib.Uri import IsAbsolute
    from Ft.Server.Server.Drivers.PathImp import RepoPathToUri
    from Ft.Server.Server.Drivers import FtssInputSource
    uri = Conversions.StringValue(uri)
    if IsAbsolute(uri):
        return uri
    if base:
        if not IsAbsolute(base):
            base = RepoPathToUri(base)
        normalized = FtssInputSource.FTSS_RESOLVER.normalize(rel, base)
        return normalized
    return RepoPathToUri(uri)


ExtFunctions = {
    #(SCORE_NS, 'repo-user-name'): RepoUserName,
    (SCORE_NS, 'has-session'): HasSession,
    (SCORE_NS, 'get-current-user'):  GetCurrentUser,
    (SCORE_NS, 'get-current-username'):  GetCurrentUserName,
    (SCORE_NS, 'get-user-name'): _GetUserName, # deprecated!
    (SCORE_NS, 'get-user-or-group'): GetUserOrGroupByName,
    #(NS, 'create'): SessionCreate,
    (SCORE_NS, 'session-invalidate'): SessionInvalidate,
    #(NS, 'reattach'): SessionReattach,
    (SCORE_NS, 'session-data'): SessionData,
    (SCORE_NS, 'delete-session-data'): DeleteSessionData,
    (SCORE_NS, 'hash-string'): HashString,
    (SCORE_NS, "get-all-document-definition-paths"): GetAllDocumentDefinitionPaths,
    (SCORE_NS, 'repo-path-to-uri'): RepoPathToUriFunc,
    (SCORE_NS, 'uri-to-repo-path'): UriToRepoPath,
    (SCORE_NS, 'normalize-uri'): NormalizeUri,
}

ExtElements = {
    (SCORE_NS, 'rollback'): RollbackElement,
    (SCORE_NS, 'login'): LoginElement,
    (SCORE_NS, 'create-session'): CreateSessionElement,
    (SCORE_NS, 'session-data'): SessionDataElement,
    (SCORE_NS, 'repo-text-search'): TextSearchRepositoryElement,
    (SCORE_NS, 'repo-text-reindex'): TextReindexRepositoryElement,
}
