"""
    MoinMoin - Email activation of new accounts

    EmailActivation causes new accounts to be disabled when created.
    They must be activated via a URL that is emailed to the address
    entered into the account is created.  Unactivated accounts
    expire and then are deleted.  This file provides all the code
    for EmailActivation - the other entry points just defer to here.
    Apart from those entry points this file provides the
    EmailActivation macro. The macro produces the HTML for shown
    when a new user clicks on the emailed URL.  The HTML varies
    depending on what information is provided, and whether it is
    correct or not.

    @copyright: 2007 by MoinMoin:RussellStuart
    @license: GNU GPL, see COPYING for details.
"""

#
# Activate a new account via email.  This contains the code for both the action
# and the macro.
#
import errno
import os
import time

from MoinMoin import user
from MoinMoin import util
from MoinMoin import wikiutil

#
# Macro entry point.
#
def execute(macro, args):
    message, html = emailActivation(macro.request, macro.form)
    return html

#
# EmailActivation action entry point.
#
def action(pagename, request):
    return emailActivation(request, request.form)

#
# Userform action entry point.
#
def userform_action(request, u):
    #
    # Generate the cookie, which should be completely unguessable.
    # If there is a good source of random numbers use it.  Otherwise
    # just use the random module.  The random module is completely
    # deterministic - if you know the time the cookie was generated
    # you can figure out what it is easily enough, so its simple to
    # hack.
    #
    try:
	urandom_file = file("/dev/urandom")
	try:
	    bytes = urandom_file.read(8)
	finally:
	    urandom_file.close()
	u.emailactivation_cookie = "".join(["%02x" % ord(c) for c in bytes])
    except EnvironmentError:
	import random
	random.seed()
        u.emailactivation_cookie = "%013x" % random.randint(0, 1 << 52)
    #
    # Now send the email.
    #
    url = "%s?n=%s&c=%s"  % (
            request.getBaseURL() + "/" + __name__.split(".")[-1],
            u.name, u.emailactivation_cookie)
    email_data = emailActivation_email('create', request, u, url)
    message = None
    if email_data != None:
        email_to, email_subject, email_text, cookie_expire, message = email_data
        u.emailactivation_expires = str(time.time() + cookie_expire)
        u.disabled = 1
        u.valid = 0
        u.save()
        mailok, msg = util.mail.sendmail(
                request, email_to, email_subject, email_text)
    return message

#
# Return the type of user we have.
#
def user_type(u):
    if not u.id:
        return USER_INVALID
    if not hasattr(u, 'emailactivation_cookie'):
        return USER_VALID
    if hasattr(u, 'emailactivation_expires'):
        if float(u.emailactivation_expires) >= time.time():
            return USER_UNACTIVATED
    return USER_PURGE
USER_INVALID, USER_VALID, USER_UNACTIVATED, USER_PURGE = range(4)

#
# Remove all expired unactivated users.
#
def purge_users(all_users):
    now = time.time()
    purge_done = 0
    for u in all_users:
        if user_type(u) != USER_PURGE:
            continue
        purge_done = 1
        try:
            os.remove(u._User__filename())
        except EnvironmentError, e:
            if e.errno != errno.ENOENT:
                raise
        try:
            os.remove(u._User__filename() + ".trail")
        except EnvironmentError, e:
            if e.errno != errno.ENOENT:
                raise
    #
    # Purge the cache if we changed something.
    #
    if not purge_done:
        return
    try:
        os.remove(os.path.join(u._cfg.data_dir, "cache/user/name2id"))
    except EnvironmentError, e:
        if e.errno != errno.ENOENT:
            raise

#
# Pages we display.
#
pages = {
  'FormActivateOrCancel': """
      <form method="POST" action="%(url)s"><font face="sans-serif">
        <p><font color="blue">
          <b>Thanks for coming back!</b>
        </font></p>
        <ul>
          <li>Name: %(user-name)s.</li>
          <li>Email: %(user-email)s.</li>
        </ul>
        <p>
          If these details are correct then click the
          <b><code>Activate</code></b> button below.
          Otherwise click the <b><code>Cancel</code></b> button.
        </p>
        <p>
          <input type="hidden" name="action" value="%(action)s">
          <input type="hidden" name="i" value="%(i)s">
          <input type="hidden" name="c" value="%(c)s">
          <input type="submit" name="submit" value="Activate">
          <input type="submit" name="submit" value="Cancel">
        </p>
      </font></form>
    """,

  'FormCancel': """
      <form method="POST" action="%(url)s"><font face="sans-serif">
        <font color="red"><p align="center">
          <b>You did not supply an authentication cookie!</b>
        </p></font>
        <p>
          This means you can't activate the new account.
          But you can cancel the attempt to create your account
          by clicking the button below.
        </p>
        <p>
          <input type="hidden" name="action" value="%(action)s">
          <input type="hidden" name="i" value="%(i)s">
          <input type="submit" name="submit" value="Cancel create account">
        </p>
      </font></form>
    """,

  'ErrorNoUser': """
      <font face="sans-serif">
        <font color="red"><p align="center">
          <b>You didn't specify what account to activate.</b>
        </p></font>
        <p>
          This wiki page is used for activateing new wiki accounts.
          As you are not doing that there is nothing to see here.
        </p>
      </font>
    """,

  'ErrorBadUser': """
      <font face="sans-serif">
        <font color="red"><p align="center">
          <b>Sorry, I can't find a matching account.</b>
        </p></font>
        <p>
          Perhaps:
          <ul>
          <li>it never was created,</li>
          <li>or its been responded to,</li>
          <li>or it has expired (they only last a week).</li>
          </ul>
        </p>
        <p>If you responded to it earlier you can try logging in.</p>
      </font>
    """,

  'ErrorBadCookie': """
      <font face="sans-serif">
        <font color="red"><p align="center">
          <b><font size="+4">EH!</font><br>
          That cookie is invalid.</b>
        </p></font>
        <p>
          Either you are trying to hack me, or the the link you followed is
          from an earlier account creation attempt that has been cancelled.
          The latter explaination seems unlikely.
        </p>
      </font>
    """,

  'ErrorBadSubmit': """
      <font face="sans-serif">
        <font color="red"><p align="center">
          <b><font size="+4">EH!</font><br>
          What the hell does '%(submit)s' mean?.</b>
        </p></font>
        <p>
          You are trying to hack me. Go away!
        </p>
      </font>
    """,

  'ErrorBadResult': """
      <font face="sans-serif">
        <font color="red"><p align="center">
          <b><font size="+4">EH!</font><br>
          What the hell does '%(r)s' mean?!?.</b>
        </p></font>
        <p>
          You are trying to hack me. Go away!
        </p>
      </font>
    """,

  'Redirect': """
      <font face="sans-serif">
        You are being redirected to
        <a href="%(redirect)s">%(redirect)s</a>.
      </font>
    """,

  'ResultCancelled': """
      <font face="sans-serif">
        <p>
          The account has been cancelled at your request.
        </p>
        <p>
          If that is not what you wanted then create it again.
        </p>
      </font>
    """,

  'ResultActivated': """
      <font face="sans-serif">
        <p>
          The account has been activated!
        </p>
        <p>
          You can log in now using the <b>Login</b> link above.
        </p>
      </font>
    """,
}

#
# The heart of this plugin.  Called by the macro to produce the HTML to
# display to a user trying to activate his account, and also called by the
# action invoked when he attempts to activate it.  Returns:
#
#   message, html
#
# The message is displayed by the action, the html is displayed by the macro.
#
def emailActivation(request, form):
    _ = getText(request)
    #
    # If this is a superuser then we do things differently.
    #
    if request.user.isSuperUser():
        return superEmailActivation(request, form)
    #
    # Read in all user files.
    #
    all_users = [
            user.User(request, id=userid)
            for userid in user.getUserList(request)]
    #
    # Get form variables.
    #
    name = form.get('n', [''])[0]
    id = form.get('i', [''])[0]
    cookie = form.get('c', [''])[0]
    submit = form.get('submit',[''])[0]
    result = form.get('r', [''])[0]
    #
    # Read in the user data.
    #
    if name:
        new_user = [u for u in all_users if u.name == name]
    elif id:
        new_user = [u for u in all_users if u.id == id]
    else:
        new_user = None
    if new_user:
        new_user = new_user[0]
    else:
        new_user = user.User(request)
        new_user.id = ''
    #
    # Calculate the variables to be inserted into the resulting page.
    #
    url = request.getScriptname() + request.getPathinfo()
    pageVars = {
            'action':	__name__.split(".")[-1],
            'c':	wikiutil.escape(cookie),
            'i':	wikiutil.escape(new_user.id),
            'r':	wikiutil.escape(result),
            'url':	url,
            'user-email': wikiutil.escape(getattr(new_user, 'email', '')),
            'user-name': wikiutil.escape(getattr(new_user, 'name', '')),
        }
    message = None
    redirect = ''
    if result and not result in pages:
        page = 'ErrorBadResult'
    elif not name and not id:
        page = 'ErrorNoUser'
    elif result:
        page = result
    elif user_type(new_user) != USER_UNACTIVATED:
        page = 'ErrorBadUser'
    elif submit and submit.split()[0].lower() == 'cancel':
        activateCancelled(request, new_user)
        purge_users(all_users)
        redirect = "%(url)s?r=ResultCancelled&i=%(i)s" % pageVars
        message = (
                _("The account creation request for %s has been cancelled.") %
                (new_user.name,))
        page = 'Redirect'
    elif not cookie:
        if submit:
            page = 'ErrorSubmit'
        else:
            page = 'FormCancel'
    elif cookie != getattr(new_user, 'emailactivation_cookie', None):
        page = 'ErrorBadCookie'
    elif submit.lower() == 'activate':
        accountActivated(request, new_user)
        purge_users(all_users)
        redirect = "%(url)s?r=ResultActivated&i=%(i)s" % pageVars
        message = (
                _("%s's account has been enabled.  You can login now.") %
                (new_user.name,))
        page = 'Redirect'
    elif submit:
        page = 'ErrorBadSubmit'
    else:
        page = 'FormActivateOrCancel'
    pageVars['redirect'] = redirect
    return message, _(pages[page]) % pageVars

#
# The version of emailActivation() that a superuser sees.
#
SUPER_HEADER = """
    <p>Hello MoinMoin super user.  Here are the new accounts awaiting activation:</p>
    <form action="%(url)s" method="POST">
      <input type="hidden" name="action" value="%(action)s">
      <table>
        <tr>
          <td><b>Name</b></td>
          <td><b>Email</b></td>
          <td><b>Expires</b></td>
          <td><b>Ignore</b></td>
          <td><b>Cancel</b></td>
          <td><b>Activate</b></td>
        </tr>
"""

SUPER_LINE = """
        <tr>
          <td>%(user.name)s</td>
          <td>%(user.email)s</td>
          <td>%(expires)s</td>
          <td align="center"><input type="radio" name="%(user.id)s" value="ignore"%(ignore_checked)s%(ignore_disabled)s></td>
          <td align="center"><input type="radio" name="%(user.id)s" value="cancel"%(cancel_checked)s></td>
          <td align="center"><input type="radio" name="%(user.id)s" value="activate"%(activate_disabled)s></td>
        </tr>
"""


def superEmailActivation(request, form):
    _ = getText(request)
    message = ''
    #
    # Save any results.
    #
    all_users = [
            user.User(request, id=userid)
            for userid in user.getUserList(request)]
    count = 0
    for u in all_users:
        result = form.get(u.id, [''])[0]
        if not result:
            continue
        if result == 'activate':
            count += accountActivated(request, u)
        if result == 'cancel':
            count += activateCancelled(request, u)
    if 'submit' in form:
        message = str(count) + ' users updated'
        purge_users(all_users)
        all_users = [
                user.User(request, id=userid)
                for userid in user.getUserList(request)]
    #
    # Generate table.
    #
    url = request.getScriptname() + request.getPathinfo()
    action = (__name__.split(".")[-1])
    html = _(SUPER_HEADER) % {"url": url, "action": action}
    count = 0
    for u in all_users:
        type = user_type(u)
        if not type in (USER_UNACTIVATED, USER_PURGE):
            continue
        count += 1
        ignore_checked    = type != USER_PURGE and " checked"  or ""
        ignore_disabled   = type == USER_PURGE and " disabled" or ""
        cancel_checked    = type == USER_PURGE and " checked"  or ""
        activate_disabled = type == USER_PURGE and " disabled" or ""
        t = float(u.emailactivation_expires)
        vars = {
                'activate_disabled':     activate_disabled,
                'expires':              time.strftime('%d/%b %H:%M', time.localtime(t)),
                'cancel_checked':       cancel_checked,
                'ignore_checked':       ignore_checked,
                'ignore_disabled':      ignore_disabled,
                'user.id':              u.id,
                'user.name':            u.name,
                'user.email':           u.email,
            }
        html += _(SUPER_LINE) % vars
    html += '  </table>\n'
    if not count:
        html += '  <p>' + _("There are no unactivated accounts.") + '</p>\n'
    else:
        html += '  <input type="submit" name="submit" value="submit">\n'
        html += '  <input type="reset" name="reset" value="reset">\n'
    html += '</form>'
    return message, html

#
# Generate the email to send to a user.  Calls the user hook in the
# configuration if there is one and inserts his customisations, using
# the defaults where there aren't any.
#
ACTIVATED_EMAIL_TEXT = """
Congradulations %(user.name)s!

Your account on the %(sitename)s has been
activated.  You can now login at this url:

    %(url)s

--
Regards,
The %(sitename)s administrator
"""

CANCELLED_EMAIL_TEXT = """
%(user.name)s's request to create an account on
the %(sitename)s has been cancelled.

--
Regards,
The %(sitename)s administrator
"""

CREATE_EMAIL_TEXT = """
Somebody (hopefully you) tried to create an account on
this wiki with your email address.  If it was you then
you can finish the process by following the link below.
If it wasn't you then you can safely ignore this email.

%(url)s

Account Details:
    Name   %(user.name)s
    Email  %(user.email)s

--
Regards,
The %(sitename)s administrator
"""

def emailActivation_email(action, request, user, url):
    _ = getText(request)
    #
    # Call the users function.
    #
    func = getattr(request.cfg, __name__.split(".")[-1] + "_email", None)
    if not func:
        custom_data = []
    else:
        custom_data = func(action, request, user, url)
        if custom_data == None:
            return None
    if action == 'cancel' and len(custom_data) == 0:
        return None
    #
    # The defaults.
    #
    to = user.email
    subject, email_text = {
            'activate': ("activated", ACTIVATED_EMAIL_TEXT),
            'cancel':   ("cancelled", CANCELLED_EMAIL_TEXT),
            'create':   ("creation",  CREATE_EMAIL_TEXT),
        }.get(action)
    subject = _("%s account %s") % (request.cfg.sitename, _(subject))
    emailVars = {
            'sitename':         request.cfg.sitename,
            'url':              url,
            'user.name':        user.name,
            'user.email':       user.email,
        }
    text = _(email_text) % emailVars
    message = None
    default_data = [to, subject, text, 7*24*60*60, message]
    #
    # Use the defaults where he didn't give us something.
    #
    custom_data = list(custom_data)
    if len(custom_data) < len(default_data):
        custom_data += [None] * (len(default_data) - len(custom_data))
    else:
        custom_data = custom_data[:len(default_data)]
    email_data = [
            (c != None and c or d) for c, d in zip(custom_data, default_data)]
    #
    # If he didn't specify a message, then create a default one
    # depending on who he is sending the email to.
    #
    if email_data[4] == None:
        if email_data[0] == user.email:
            message = "You should receive an email with further instructions shortly!"
        else:
            message = "Your new account is waiting for the wiki administrator's for approval."
        email_data[4] = _(message)
    #
    # Make sure to is a list.
    #
    if type(email_data[0]) != type(()):
        email_data[0] = [email_data[0]]
    return email_data

#
# Activate the user passed.  Returns 1 if the account was activated, 0
# otherwise.  purge_users() should be called after this.
#
def accountActivated(request, u):
    modified = 0
    if hasattr(u, 'emailactivation_expires'):
        del u.emailactivation_expires
        modified = 1
    if hasattr(u, 'emailactivation_cookie'):
        del u.emailactivation_cookie
        modified = 1
    if modified and u.disabled:
        u.disabled = 0
    if modified:
        u.save()
        url = "%s/UserPreferences?action=login" % (request.getBaseURL())
        email_data = emailActivation_email('activate', request, u, url)
        if email_data != None:
            email_to, email_subject, email_text, cookie_expire, message = email_data
            mailok, msg = util.mail.sendmail(
                  request, email_to, email_subject, email_text)
    return modified

#
# Cancel the activation request for the user passed.  Returns 1 if the
# account activation was cancelled, 0 otherwise.  purge_users() should
# be called after this.
#
def activateCancelled(request, u):
    if not hasattr(u, 'emailactivation_cookie'):
        return 0
    if hasattr(u, 'emailactivation_expires'):
        del u.emailactivation_expires
        email_data = emailActivation_email('cancel', request, u, '')
        if email_data != None:
            email_to, email_subject, email_text, cookie_expire, message = email_data
            mailok, msg = util.mail.sendmail(
                  request, email_to, email_subject, email_text)
    return 1

def getText(request):
    return lambda str: request.getText(str, formatted=False)

#
# Debugging only
#
def dump(structure,title=None,maxdepth=None):
    import os
    import sys
    sbin = '/usr/local/lubemobile/sbin'
    if not sbin in sys.path:
        sys.path.append(sbin)
    import rasutils
    handle = file("/tmp/moin.log", "a")
    os.chmod("/tmp/moin.log", 0666)
    d = rasutils.deeprepr(structure,maxdepth=maxdepth)
    if title != None:
        d = title + ": " + d
    handle.write(d + "\n")
    handle.close()
