"""
This module contains code for the publicly-visible areas of a Mantissa
server's web interface.
"""

from zope.interface import implements

from twisted.internet import defer
from twisted.python import util

from nevow import rend, athena, tags, inevow, static
from nevow.url import URL

from axiom import item, attributes, upgrade, userbase

from xmantissa import ixmantissa, website, offering
from xmantissa.webtheme import getLoader, getInstalledThemes
from xmantissa.ixmantissa import IStaticShellContent

class PublicWeb(item.Item, website.PrefixURLMixin):
    """
    Fixture for site-wide public-facing content.

    I implement ISiteRootPlugin and use PrefixURLMixin; see the documentation
    for each of those for a detailed explanation of my usage.

    I adapt another object to IPublicPage, call the public page's
    createResource() method, and display that resource.

    This is designed to be installed on a user who has some public facing
    content.  There are two contexts where a public page is useful: at the top
    level of a site, via a 'system user', and for the public facing view of a
    user's store who has a private view of that data using
    L{webapp.PrivateApplication}.

    For the former case, for example to put some dynamic content on the root
    page of a public site, the convention is to create an avatar (with a
    substore) to represent the public portion of your application and then wrap
    a PublicWeb around it as the plugin in the top-level store.  Example::

        s = Store("my-site.axiom")
        # Install login database
        ls = LoginSystem(store=s)
        # Install HTTP server
        WebSite(store=s, portNumber=8080, securePortNumber=8443,
                certificateFile='server.pem').installOn(s)

        # Add 'system user' to hold data that will be displayed on the public page.
        mySiteSystemUser = ls.addAccount('my-site', 'my-site.example.com', None)
        # Open the substore that was automatically created for us
        substore = mySiteSystemUser.avatars.open()
        # Install your custom application public page on the substore, so that
        # PublicWeb will find the IPublicPage implementor when it adapts
        substore.powerUp(MySitePublicPage(store=substore),
                         IPublicPage)
        # Install the PublicWeb on the top-level store, as a plugin for the
        # WebSite installed above.
        PublicWeb(store=s,
                  sessionless=True,  # Alternatively, sessioned=True
                  prefixURL=u'path/to/my-site',
                  application=mySiteSystemUser).installOn(s)


    @ivar application: An Item which implements L{ixmantissa.IPublicPage}.
    """
    implements(ixmantissa.ISiteRootPlugin,
               ixmantissa.ISessionlessSiteRootPlugin)

    typeName = 'mantissa_public_web'
    schemaVersion = 3

    prefixURL = attributes.text(
        doc="""
        The prefix of the URL where objects represented by this fixture will
        appear.  For the front page this is u'', for other pages it is their
        respective URLs.
        """, allowNone=False)

    application = attributes.reference(
        doc="""
        An Item which is adaptable to L{ixmantissa.IPublicPage}.
        """,
        allowNone=False)

    installedOn = attributes.reference(
        doc="""
        """)

    sessioned = attributes.boolean(
        doc="""
        Will this resource be provided to clients with a session?  Defaults to
        False.
        """,
        default=False)

    sessionless = attributes.boolean(
        doc="""
        Will this resource be provided without a session to clients without a
        session?  Defaults to False.
        """,
        default=False)


    def resourceFactory(self, segments):
        """
        Reserve names which begin with '__' for the framework (such as __login__,
        __logout__, __session__, etc), but delegate everything else to
        PrefixURLMixin (and createResource) as usual.
        """
        if not segments[0].startswith('__'):
            return super(PublicWeb, self).resourceFactory(segments)
        return None


    def createResource(self):
        """
        When invoked by L{PrefixURLMixin}, adapt my application object to
        L{IPublicPage} and call C{getResource} on it.
        """
        return ixmantissa.IPublicPage(self.application).getResource()


def upgradePublicWeb1To2(oldWeb):
    newWeb = oldWeb.upgradeVersion(
        'mantissa_public_web', 1, 2,
        prefixURL=oldWeb.prefixURL,
        application=oldWeb.application,
        installedOn=oldWeb.installedOn)
    newWeb.installedOn.powerUp(newWeb, ixmantissa.ICustomizablePublicPage)
    return newWeb
upgrade.registerUpgrader(upgradePublicWeb1To2, 'mantissa_public_web', 1, 2)

def upgradePublicWeb2To3(oldWeb):
    newWeb = oldWeb.upgradeVersion(
        'mantissa_public_web', 2, 3,
        prefixURL=oldWeb.prefixURL,
        application=oldWeb.application,
        installedOn=oldWeb.installedOn,
        # There was only one PublicWeb before, and it definitely
        # wanted to be sessioned.
        sessioned=True)
    newWeb.installedOn.powerDown(newWeb, ixmantissa.ICustomizablePublicPage)
    other = newWeb.installedOn
    newWeb.installedOn = None
    newWeb.installOn(other)
    return newWeb
upgrade.registerUpgrader(upgradePublicWeb2To3, 'mantissa_public_web', 2, 3)



class _CustomizingResource(object):
    """
    _CustomizingResource is a wrapping resource used to implement
    CustomizedPublicPage.

        There is an implementation assumption here, which is that the top
        _CustomizingResource is always at "/", and locateChild will always be
        invoked at least once.  If this doesn't hold, this render method might
        be invoked on the top level _CustomizingResource, which would cause it
        to be rendered without customization.  If you're going to use this
        class directly for some reason, please keep this in mind.
    """
    implements(inevow.IResource)

    def __init__(self, topResource, forWho):
        """
        Create a _CustomizingResource.

        @param topResource: an L{inevow.IResource} provider, who may also
        provide L{ixmantissa.ICustomizable} if it wishes to be customized.

        @param forWho: the external ID of the currently logged-in user.
        @type forWho: unicode
        """
        self.currentResource = topResource
        self.forWho = forWho


    def locateChild(self, ctx, path):
        """
        Return a Deferred which will fire with the customized version of the
        resource being located.
        """
        D = defer.maybeDeferred(
            self.currentResource.locateChild, ctx, path)

        def finishLocating((nextRes, nextPath)):
            custom = ixmantissa.ICustomizable(nextRes, None)
            if custom is not None:
                return (custom.customizeFor(self.forWho), nextPath)
            self.currentResource = nextRes
            if nextRes is None:
                return (nextRes, nextPath)
            return (_CustomizingResource(nextRes, self.forWho), nextPath)

        return D.addCallback(finishLocating)


    def renderHTTP(self, ctx):
        """
        Render the resource I was provided at construction time.
        """
        if self.currentResource is None:
            return rend.FourOhFour()
        return self.currentResource # nevow will automatically adapt to
                                    # IResource and call rendering methods.


class CustomizedPublicPage(item.Item):
    """
    A CustomizedPublicPage is a powerup for users which is installed on their
    stores at the URL '/'.  It finds the real site's public-page and asks it to
    customize itself for the user that its store belongs to.

    This works because when users are logged in, their top level resource is
    their own store, and so this powerup has a chance to intercept any hit to
    any URL on the entire site before it is actually rendered to the user.

    While this technique for logging in requires a user store, the only
    information it communicates to other stores is a string via the
    C{customizeFor} method, so other implementations of this technique could
    simply be drawing that string from a cookie, from OpenID, etc.  Those
    interested in implementing distributed security mechanisms for mantissa
    should familiarize themselves with this code.
    """

    typeName = 'mantissa_public_customized'
    schemaVersion = 2

    installedOn = attributes.reference(
        doc="""
        The Avatar for which this item will attempt to retrieve a customized
        page.
        """)

    powerupInterfaces = [(ixmantissa.ISiteRootPlugin, -257)]

    def resourceFactory(self, segments):
        """
        Implementation of L{ixmantissa.ISiteRootPlugin.resourceFactory}.

        This will look in this powerup's store to discover the currently active
        username, look for the parent store's top-level resource (the top-level
        resource of the site) and then return a L{_CustomizingResource} which
        wraps that resource and customizes any of the customizable children.

        @return: a 2-tuple of a customizing resource and the given list of
        segments, or, if no resource is provided by the site store, None.
        """
        topResource = inevow.IResource(self.store.parent, None)
        if topResource is not None:
            for resource, domain in userbase.getAccountNames(self.store):
                username = '%s@%s' % (resource, domain)
                break
            else:
                username = None
            return (_CustomizingResource(topResource, username), segments)
        return None



def customizedPublicPage1To2(oldPage):
    newPage = oldPage.upgradeVersion(
        'mantissa_public_customized', 1, 2,
        installedOn=oldPage.installedOn)
    newPage.installedOn.powerDown(newPage, ixmantissa.ISiteRootPlugin)
    newPage.installedOn.powerUp(newPage, ixmantissa.ISiteRootPlugin, -257)
    return newPage
upgrade.registerUpgrader(customizedPublicPage1To2, 'mantissa_public_customized', 1, 2)



class PublicPageMixin(object):
    """
    Mixin for use by C{Page} or C{LivePage} subclasses that are visible to
    unauthenticated clients.

    @ivar needsSecure: whether this page requires SSL to be rendered.
    """
    fragment = None
    title = ''
    username = None
    needsSecure = False

    def renderHTTP(self, ctx):
        """
        Issue a redirect to an HTTPS URL for this resource if one
        exists and the page demands it.
        """
        req = inevow.IRequest(ctx)
        securePort = inevow.IResource(self.store).securePort
        if self.needsSecure and not req.isSecure() and securePort is not None:
            return URL.fromContext(
                ctx).secure(port=securePort.getHost().port)
        else:
            return super(PublicPageMixin, self).renderHTTP(ctx)


    def render_navigation(self, ctx, data):
        """
        Return the empty string; do not provide the public site with any
        navigation.
        """
        return ""


    def render_search(self, ctx, data):
        """
        Return the empty string; do not provide the public site with any search
        functionality.
        """
        return ""


    def render_title(self, ctx, data):
        """
        Return the current context tag containing the 'title' attribute of this
        page.
        """
        return ctx.tag[self.title]


    def render_username(self, ctx, data):
        """
        If this is being viewed by a logged in user, fill the 'username' slot with
        their username.

        Otherwise, discover available public signup mechanisms and fill the
        'signups' slot with a list of them.
        """
        # there is a circular import here which should probably be avoidable,
        # since we don't actually need signup links on the signup page.  on the
        # other hand, maybe we want to eventually put those there for
        # consistency.  for now, this import is easiest, and although it's a
        # "friend" API, which I dislike, it doesn't seem to cause any real
        # problems...  -glyph
        from xmantissa.signup import _getPublicSignupInfo

        if self.username is not None:
            return ctx.tag.fillSlots('username', self.username)

        IQ = inevow.IQ(self.docFactory)
        signupPattern = IQ.patternGenerator('signup')
        loginLinks = IQ.onePattern('login-links')

        signups = []
        for (prompt, url) in _getPublicSignupInfo(self.store):
            signups.append(signupPattern.fillSlots(
                    'prompt', prompt).fillSlots(
                    'url', url))

        return ctx.tag.clear()[loginLinks.fillSlots('signups', signups)]


    def render_header(self, ctx, data):
        """
        Render any required static content in the header, from the C{staticContent}
        attribute of this page.
        """
        if self.staticContent is None:
            return ctx.tag

        header = self.staticContent.getHeader()
        if header is not None:
            return ctx.tag[header]
        else:
            return ctx.tag

    def render_footer(self, ctx, data):
        """
        Render any required static content in the footer, from the C{staticContent}
        attribute of this page.
        """
        if self.staticContent is None:
            return ctx.tag

        header = self.staticContent.getFooter()
        if header is not None:
            return ctx.tag[header]
        else:
            return ctx.tag


    def render_content(self, ctx, data):
        """
        This renderer, which is used for the visual bulk of the page, provides
        self.fragment renderer.
        """
        return ctx.tag[self.fragment]


    def head(self):
        """
        Override this method to insert additional content into the header.  By
        default, does nothing.
        """
        return None


    def getHeadContent(self, req):
        """
        Retrieve a list of header content from all installed themes on the site
        store.
        """
        website = inevow.IResource(self.store)
        for t in getInstalledThemes(self.store):
            yield t.head(req, website)


    def render_head(self, ctx, data):
        """
        This renderer calculates content for the <head> tag by concatenating the
        values from L{getHeadContent} and the overridden L{head} method.
        """
        req = inevow.IRequest(ctx)
        return ctx.tag[filter(None, list(self.getHeadContent(req)) + [self.head()])]



class PublicPage(PublicPageMixin, rend.Page):
    """
    PublicPage is a utility superclass for implementing static pages which have
    theme support and authentication trimmings.
    """

    def __init__(self, original, store, fragment, staticContent, forUser):
        """
        Create a public page.

        @param original: any object

        @param fragment: a L{rend.Fragment} to display in the content area of
        the page.

        @param staticContent: some stan, to include in the header of the page.

        @param forUser: a string, the external ID of a user to customize for.
        """
        super(PublicPage, self).__init__(original, docFactory=getLoader("public-shell"))
        self.store = store
        self.fragment = fragment
        self.staticContent = staticContent
        if forUser is not None:
            assert isinstance(forUser, unicode), forUser
        self.username = forUser



class _OfferingsFragment(rend.Fragment):
    """
    This fragment provides the list of installed offerings as a data generator.
    This is used to display the list of app stores on the default front page.
    """
    def __init__(self, original):
        """
        Create an _OfferingsFragment with an item from a site store.

        @param original: a L{FrontPage} item.
        """
        super(_OfferingsFragment, self).__init__(
            original, docFactory=getLoader('front-page'))


    def data_offerings(self, ctx, data):
        """
        Generate a list of installed offerings.

        @return: a generator of dictionaries mapping 'name' to the name of an
        offering installed on the store.
        """
        for io in self.original.store.query(offering.InstalledOffering):
            pp = ixmantissa.IPublicPage(io.application, None)
            if pp is not None and getattr(pp, 'index', True):
                yield {
                    'name': io.offeringName,
                    }



class PublicFrontPage(PublicPage):
    """
    This is the implementation of the default Mantissa front page.  It renders
    a list of offering names, displays the user's name, and lists signup
    mechanisms.  It also provides various top-level URLs.
    """
    implements(ixmantissa.ICustomizable)

    def __init__(self, original, staticContent, forUser=None):
        """
        Create a PublicFrontPage.

        @param original: a L{FrontPage} item, which we use primarily to get at a Store.

        @param staticContent: additional data to embed in the header.

        @param forUser: an external ID of the logged in user, or None, if the
        user viewing this page is browsing anonymously.
        """
        PublicPage.__init__(
            self, original, original.store, _OfferingsFragment(original),
            staticContent, forUser)


    def locateChild(self, ctx, segments):
        """
        Look up children in the normal manner, but then customize them for the
        authenticated user if they support the L{ICustomizable} interface.  If
        the user is attempting to access a private URL, redirect them.
        """
        result = super(PublicFrontPage, self).locateChild(ctx, segments)
        if result is not rend.NotFound:
            child, segments = result
            if self.username is not None:
                cust = ixmantissa.ICustomizable(child, None)
                if cust is not None:
                    return cust.customizeFor(self.username), segments
            return child, segments

        # If the user is trying to access /private/*, then his session has
        # expired or he is otherwise not logged in. Redirect him to /login,
        # preserving the URL segments, rather than giving him an obscure 404.
        if segments[0] == 'private':
            u = URL.fromContext(ctx).click('/').child('login')
            for seg in segments:
                u = u.child(seg)
            return u, ()

        return rend.NotFound


    def childFactory(self, ctx, name):
        """
        Customize child lookup such that all installed offerings on the site store
        that this page is viewing are given an opportunity to display their own
        page.
        """
        offer = self.original.store.findFirst(
            offering.InstalledOffering,
            offering.InstalledOffering.offeringName == unicode(name, 'ascii'))
        if offer is not None:
            pp = ixmantissa.IPublicPage(offer.application, None)
            if pp is not None:
                return pp.getResource()
        return None


    def child_(self, ctx):
        """
        Return 'self' if the index is requested, since this page can render a
        simple index.
        """
        return self


    def child_by(self, ctx):
        """
        Return a L{UserIndexPage} for the URL '/by'.
        """
        from xmantissa.websharing import UserIndexPage
        return UserIndexPage(self.original.store.findUnique(userbase.LoginSystem))


    def child_Mantissa(self, ctx):
        """
        Serve files from C{xmantissa/static/} at the URL C{/Mantissa}.
        """
        # Cheating!  It *looks* like there's an app store, but there isn't
        # really, because this is the One Store To Bind Them All.
        return static.File(util.sibpath(__file__, "static"))


    def customizeFor(self, forUser):
        """
        Return a customized version of this page for a particular user.

        @param forUser: the external ID of a user.
        """
        return PublicFrontPage(self.original, self.staticContent, forUser)


    def renderHTTP(self, ctx):
        """
        If viewed by a logged in user, redirect them to their application at
        /private.  Otherwise, view the public index page.
        """
        if self.username:
            self.original.publicViews += 1
            return URL.fromContext(ctx).click('/').child('private')
        else:
            self.original.privateViews += 1
            return PublicPage.renderHTTP(self, ctx)



class LoginPage(PublicPage):
    """
    This is the page which presents a 'login' dialog to the user, at "/login".

    This does not perform the actual login, nevow.guard does that, at the URL
    /__login__; this resource merely provides the entry field and redirection
    logic.
    """

    def __init__(self, original, store, segments=()):
        """
        Create a login page.

        @param original: unused.

        @param store: a site store which contains a WebSite item.

        @param segments: a list of strings.  For example, if you hit
        /login/private/stuff, you want to log in to /private/stuff, and the
        resulting LoginPage will have the segments of ['private', 'stuff']
        """
        PublicPage.__init__(self, original, store, getLoader("login"),
                            IStaticShellContent(original.store, None),
                            None)
        self.segments = segments


    def beforeRender(self, ctx):
        """
        Before rendering this page, identify the correct URL for the login to post
        to, and the error message to display (if any), and fill the 'login
        action' and 'error' slots in the template accordingly.
        """
        url = URL.fromContext(ctx).click('/')

        ws = self.store.findFirst(website.WebSite)

        if ws.securePort is not None:
            url = url.secure(port=ws.securePort.getHost().port)

        url = url.child('__login__')
        for seg in self.segments:
            url = url.child(seg)

        req = inevow.IRequest(ctx)
        err = req.args.get('login-failure', ('',))[0]

        if 0 < len(err):
            error = inevow.IQ(
                        self.fragment).onePattern(
                                'error').fillSlots('error', err)
        else:
            error = ''

        ctx.fillSlots("login-action", url)
        ctx.fillSlots("error", error)


    def locateChild(self, ctx, segments):
        """
        Return a clone of this page that remembers its segments, so that URLs like
        /login/private/stuff will redirect the user to /private/stuff after
        login has completed.
        """
        return self.__class__(self.original, self.store, segments), ()



class FrontPage(item.Item, website.PrefixURLMixin):
    """
    I am a factory for the dynamic resource L{PublicFrontPage}
    """
    implements(ixmantissa.ISiteRootPlugin)
    typeName = 'mantissa_front_page'
    schemaVersion = 1

    sessioned = True

    publicViews = attributes.integer(
        doc="""
        The number of times this object has been viewed in a public
        (non-authenticated) context.  This includes renderings of the front
        page only.
        """,
        default=0)

    privateViews = attributes.integer(
        doc="""
        The number of times this object has been viewed in a private
        (authenticated) context.  This only counts the number of times users
        have been redirected from "/" to "/private".
        """,
        default=0)

    prefixURL = attributes.text(
        doc="""
        See L{website.PrefixURLMixin}.
        """,
        default=u'',
        allowNone=False)


    def createResource(self):
        """
        Create a L{PublicFrontPage} resource wrapping this object.
        """
        return PublicFrontPage(self, None)



class PublicAthenaLivePage(PublicPageMixin, athena.LivePage):
    """
    PublicAthenaLivePage is a publicly viewable Athena-enabled page which slots
    a single fragment into the center of the page.
    """
    fragment = None
    def __init__(self, store, fragment, staticContent=None, forUser=None,
                 *a, **k):
        """
        Create a PublicAthenaLivePage.

        @param store: an L{axiom.store.Store}.

        @param fragment: The L{INavigableFragment} provider which will be
        displayed on this page.
        """
        self.store = store
        super(PublicAthenaLivePage, self).__init__(
            docFactory=getLoader("public-shell"),
            *a, **k)
        if fragment is not None:
            self.fragment = fragment
            # everybody who calls this has a different idea of what 'fragment'
            # means - let's just be super-lenient for now
            if getattr(fragment, 'setFragmentParent', False):
                fragment.setFragmentParent(self)
            else:
                fragment.page = self
        self.staticContent = staticContent
        if forUser is not None:
            assert isinstance(forUser, unicode), forUser
        self.username = forUser


    def render_head(self, ctx, data):
        """
        Put liveglue content into the header of this page to activate it, but
        otherwise delegate to my parent's renderer for <head>.
        """
        ctx.tag[tags.invisible(render=tags.directive('liveglue'))]
        return PublicPageMixin.render_head(self, ctx, data)


    def locateChild(self, ctx, segments):
        """
        Delegate locateChild to my fragment.  Wrap the resulting fragment object in
        a new PublicAthenaLivePage and return it.
        """
        res = rend.NotFound

        if hasattr(self.fragment, 'locateChild'):
            res = self.fragment.locateChild(ctx, segments)

        if res is rend.NotFound:
            res = super(PublicAthenaLivePage, self).locateChild(ctx, segments)

        return res


syntax highlighted by Code2HTML, v. 0.9.1