# -*- coding: UTF-8 -*-


# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


"""Login/Password Accounts Module"""


import errno
import os

import expression.core.dataholders as dataholders
import expression.core.directories as directories
import expression.core.elements as elements
import expression.core.environs as environs
import expression.core.faults as faults
import expression.core.filesystems as filesystems
import expression.core.locations as locations
import expression.core.logs as logs
import expression.core.modules as modules
import expression.core.namespaces as namespaces
import expression.core.sessions as sessions
import expression.core.stations as stations
import expression.core.strings as strings
import expression.core.things as things
import emails
import xforms.descriptions as descriptions

from identities import Identity


class LoginPage(descriptions.Page):
    def newContext(self, specimen, *attributes, **keywords):
        return LoginPageContext(self, specimen, *attributes, **keywords)

    def submitValidateAndProcessContext(self, context):
        self.submitContext(context)
        self.validateSubmittedContext(context)
        modelContext = context.xformsDefaultModel
        if modelContext.errors:
            return context.callHttpFunction(context.doHttpGet)

        specimen = context.specimen
        nextUrl = specimen.nextUrl
        if not nextUrl:
            nextUrl = "/"
        submission = environs.getVar("submission")
        if submission.getField("_action") == "emailPassword":
            # Alert the user that its password has been sent by email.
            nextUrl = specimen.constructUri(nextUrl, preserveAbsolutePath = True)
            return environs.getVar("httpRequestHandler").outputAlert(
                N_("An email containing your password as been sent to %s") % specimen.email,
                title = N_("Password sent by email"), url = nextUrl)
        else:
            # Log the user in.
            passwordAccountLocalId = strings.simplify(specimen.email)
            passwordAccounts = specimen.previous.previous
            passwordAccountHolder = passwordAccounts.walk([passwordAccountLocalId])
            passwordAccount = passwordAccountHolder.getRootElement()
            user = passwordAccount.getUser()
            session = sessions.getOrCreateSession()
            session.publishToken = True
            session.setAccountAbsolutePath(passwordAccount.getAbsolutePath())
            user.setSessionToken(session.getToken())
            user.getDocument().save()
            baseEnviron = environs.get(_level = "handleHttpCommand")
            baseEnviron.setVar("user", user)
            logs.debug("""Setting session user = "%s" at location "%s".""" % (
                user.simpleLabel, session.getAccountAbsolutePath()))
            # Note: constructUri must be called after getOrCreateSession, so
            # that nextUrl contains the sessionToken.
            nextUrl = specimen.constructUri(nextUrl, preserveAbsolutePath = True)
            return environs.getVar('httpRequestHandler').outputRedirect(
                nextUrl, station = specimen)

    def validateSubmittedContext(self, context):
        modelContext = context.xformsDefaultModel
        submission = environs.getVar("submission")
        if submission.getField("_action") == "emailPassword":
            # Try to send the user's password by email.
            if modelContext.errors and len(modelContext.errors) == 1 \
                    and "yep_password" in modelContext.errors:
                del modelContext.errors
            if not modelContext.errors:
                specimen = context.specimen
                passwordAccountLocalId = strings.simplify(specimen.email)
                passwordAccounts = specimen.previous.previous
                accountExists = passwordAccounts.walk([passwordAccountLocalId], None, "exists")
                if not accountExists:
                    modelContext.setErrorLabel(
                        "yep_email", "badValue", N_("Unknown email address"))
                    return
                messageHolder = passwordAccounts.walk(
                    [passwordAccountLocalId, "sendPasswordEmail"])
                message = messageHolder.prototype
                try:
                    message.sendContext(context)
                except: # TODO: tighter check
                    logs.exception("SMTP send failed")
                    modelContext.setErrorLabel("yep_email", "badValue", N_("Bad email address"))
                    return
        elif not modelContext.errors:
            # Check that the email and password are correct.
            specimen = context.specimen
            passwordAccountLocalId = strings.simplify(specimen.email)
            passwordAccounts = specimen.previous.previous
            try:
                passwordAccountHolder = passwordAccounts.walk([passwordAccountLocalId])
            except faults.PathNotFound:
                modelContext.setErrorLabel("yep_email", "badValue", N_("Unknown email address"))
                return
            passwordAccount = passwordAccountHolder.getRootElement()
            if specimen.password != passwordAccount.password:
                modelContext.setErrorLabel("yep_password", "badValue", N_("Wrong password"))
                return
            user = passwordAccount.getUser()
            if user is None:
                logs.warning('Account "%s" without user.' % passwordAccount)
                modelContext.setErrorLabel("yep_email", "badValue", N_("Unknown user"))
                return


class LoginPageContext(descriptions.PageContext):
    def styled(self):
        submission = environs.getVar("submission")
        nextUrl = submission.getField("nextUrl")
        if nextUrl:
            self.specimen.nextUrl = nextUrl
        super(LoginPageContext, self).styled()

    def unstyled(self):
        submission = environs.getVar("submission")
        nextUrl = submission.getField("nextUrl")
        if nextUrl:
            self.specimen.nextUrl = nextUrl
        super(LoginPageContext, self).unstyled()

    def getHttpPostUri(self):
        uri = self.getUri()
        virtualHost = environs.getVar("virtualHost")
        if not virtualHost.isSsl \
               and virtualHost.brotherVirtualHost is not None:
            # We are currently in HTTP => Direct the POST to HTTPS.
            uriAuthority = environs.getVar("uriAuthority")
            i = uriAuthority.find("@") + 1
            j = uriAuthority.find(":", i)
            if j >= 0:
                uriAuthority = uriAuthority[:j]
            httpsPort = virtualHost.brotherVirtualHost.getPort()
            if httpsPort != 443:
                uriAuthority = "%s:%d" % (uriAuthority, httpsPort)
            uri = "https://%s%s" % (uriAuthority, uri)
        return uri


class NewPage(descriptions.Page):
    def submitValidateAndProcessContext(self, context):
        self.submitContext(context)
        self.validateSubmittedContext(context)
        modelContext = context.xformsDefaultModel
        if modelContext.errors:
            return context.callHttpFunction(context.doHttpGet)
        self.validateSubmittedContextOnceAgain(context)
        if modelContext.errors:
            return context.callHttpFunction(context.doHttpGet)
        self.saveSubmittedData(context)
        self.sendHttpAnswer(context)

    def validateSubmittedContext(self, context):
        modelContext = context.xformsDefaultModel
        if not modelContext.errors or "yep_email" not in modelContext.errors:
            specimen = context.specimen
            accountExists = specimen.getUriPrevious().walk(
                [strings.simplify(specimen.email)], None, "exists")
            if accountExists:
                modelContext.setErrorLabel(
                    "yep_email", "valueNotUnique",
                    N_("Email address already used"))

    def validateSubmittedContextOnceAgain(self, context):
        specimen = context.specimen
        specimen.password = strings.makePassword(length = 6)
        messageHolder = specimen.walk(["newAccountEmail"])
        message = messageHolder.getRootElement()
        try:
            message.send()
        except: # TODO: tighter check
            logs.exception("SMTP send failed")
            modelContext = context.xformsDefaultModel
            modelContext.setErrorLabel(
                "yep_email", "badValue", N_("Bad email address"))


class PasswordAccount(things.Thing):
    _user = None
    isAccount = True # Constant

    def deleteNextUrl(self):
        nodes = self.evaluateXpath("yep:nextUrl")
        if nodes:
            node = nodes[0]
            node.unlinkNode()
            node.freeNode()

    def deleteUserPath(self):
        nodes = self.evaluateXpath("yep:user")
        if nodes:
            node = nodes[0]
            node.unlinkNode()
            node.freeNode()

    def getAuthenticationMethod(self):
        return "password"

    def getEmail(self):
        # For HTTP authentication, a user id (ie a login) can not contain ":", because ":" is used
        # as a separator between login and password.
        nodes = self.evaluateXpath("yep:email")
        if nodes:
            return nodes[0].content
        else:
            return None

    def getNextUrl(self):
        nodes = self.evaluateXpath("yep:nextUrl")
        if nodes:
            return nodes[0].content
        else:
            return None

    def getPassword(self):
        nodes = self.evaluateXpath("yep:password")
        if nodes:
            return nodes[0].content
        else:
            return None

    def getUser(self):
        if self._user is None:
            userAbsolutePath = self.getUserAbsolutePath()
            if userAbsolutePath:
                try:
                    userHolder = dataholders.DataHolder(
                        pathFragment = userAbsolutePath, mimeType = "text/xml", isRootElder = True,
                        containedFileSystem = filesystems.PartialFileSystem(userAbsolutePath))
                except IOError, error:
                    if error.errno == errno.ENOENT:
                        logs.debug("""User at path "%s" doesn't exist.""" % userAbsolutePath)
                        self._user = "none"
                    else:
                        raise
                else:
                    # Some times, users are stored in independant files; some times they are
                    # embedded inside accounts.
                    accountOrUser = userHolder.getRootElement()
                    self._user = accountOrUser.getUser()
            else:
                userLocation = self.getUserLocation()
                if userLocation:
                    try:
                        doc = self.walkToLocation(userLocation)
                    except faults.PathNotFound:
                        self._user = "none"
                    else:
                        self._user = doc.getRootElement()
                else:
                    userNodes = self.evaluateXpath("yep:user")
                    if userNodes:
                        self._user = elements.newElement(userNodes[0], previous = self, owner = self)
                    else:
                        self._user = "none"
        if self._user == "none":
            return None
        return self._user

    def getUserAbsolutePath(self):
        nodes = self.evaluateXpath("yep:user/@src")
        if nodes:
            userPath = nodes[0].content
            return self.convertPathToAbsolute(userPath)
        else:
            return None

    def getUserLocation(self):
        nodes = self.evaluateXpath("yep:user/@href")
        if nodes:
            return nodes[0].content
        else:
            return None

    def setNextUrl(self, nextUrl):
        self.setElementContent("yep:nextUrl", nextUrl)

    def setPassword(self, password):
        self.setElementContent("yep:password", password)

    def setUserAbsolutePath(self, userAbsolutePath):
        userBaseRelativePath = self.convertAbsolutePathToBaseRelative(userAbsolutePath)
        self.setElementAttribute("yep:user", "src", userBaseRelativePath)

    def setUserUri(self, userUri):
        self.setElementAttribute("yep:user", "href", userUri)

    email = property(getEmail)
    nextUrl = property(getNextUrl, setNextUrl, deleteNextUrl)
    password = property(getPassword, setPassword)


class PasswordAccountHolder(dataholders.XmlHolder):
    def deletePublicName(self):
        pass

    def getPublicName(self):
        email = self.getRootElement().email
        if email is None:
            return None
        publicName = strings.simplify(email)
        if self.isRootElder:
            publicName = os.path.join(self.getDirectoryAbsolutePath(), publicName)
        return publicName

    def setPublicName(self, publicName):
        pass

    publicName = property(getPublicName, setPublicName, deletePublicName)


class PasswordAccountsHolder(directories.DirectoryHolder):
    def login(self, *uriPathFragments):
        """
        Handles HTTP GET & POST.
        """

        command = environs.getVar("httpCommand")
        if command not in ("GET", "POST"):
            raise faults.PathNotFound("")

        try:
            elementFeature = modules.getElementFeature(namespaces.yep.uri, "passwordAccount")
        except (KeyError, ValueError):
            raise faults.PathNotFound("")
        xmlHolder = elementFeature.newXmlHolder(self, pathFragment = "login")
        return xmlHolder.checkAccessAndWalk(
            ("login", ) + uriPathFragments, command, environs.getVar("instruction"))


elements.registerElement(
    namespaces.yep.uri, "passwordAccount", PasswordAccount,
    "http://www.entrouvert.org/expression/schemas/PasswordAccount.xsd",
    "http://www.entrouvert.org/expression/descriptions/PasswordAccount.xml", PasswordAccountHolder)
