# w.c.s. - web application for online forms
# Copyright (C) 2005-2010  Entr'ouvert
#
# 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, see <http://www.gnu.org/licenses/>.

import random
import time
import csv

from .base import AuthMethod, NoSuchMethodForUserError

from quixote import redirect, get_publisher
from quixote.directory import AccessControlled, Directory
from quixote.html import TemplateIO, htmltext

from .. import _, ngettext
from ..form import *

from ..publisher import get_publisher_class
from .. import misc, get_cfg, get_logger
from .. import emails
from .. import template
from .. import tokens
from .. import errors

from ..admin.menu import command_icon

from ..backoffice.menu import html_top

from ..admin.emails import EmailsDirectory
from wcs.qommon.admin.texts import TextsDirectory

from ..cron import CronJob
from ..afterjobs import AfterJob
from .. import storage as st

from .password_accounts import PasswordAccount, HASHING_ALGOS


def notify_admins_user_registered(account):
    identities_cfg = get_cfg('identities', {})
    admins = [x for x in get_publisher().user_class.select([st.Equal('is_admin', True)])]
    if not admins:
        return
    admin_emails = [x.email for x in admins if x.email]

    user = get_publisher().user_class().get(account.user_id)
    data = {
        'hostname': get_request().get_server(),
        'username': account.id,
        'email_as_username': str(identities_cfg.get('email-as-username', False)),
        'name': user.get_display_name(),
        'email': user.email,
    }

    emails.custom_template_email('new-registration-admin-notification', data,
            admin_emails, fire_and_forget = True)


def make_password(min_len=None, max_len=None):
    passwords_cfg = get_cfg('passwords', {})
    if min_len is None:
        min_len = passwords_cfg.get('min_length', 0)
    if max_len is None:
        max_len = passwords_cfg.get('max_length', 0)
    if min_len and max_len:
        length = (min_len + max_len) / 2
    elif min_len:
        length = min_len
    elif max_len:
        length = min(max_len, 6)
    else:
        length = 6
    r = random.SystemRandom()
    return ''.join([r.choice('abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ2345678923456789') for x in range(length)])


def check_password(form, widget_name):
    passwords_cfg = get_cfg('passwords', {})
    password = form.get_widget(widget_name).parse()

    set_errors = []
    min_len = passwords_cfg.get('min_length', 0)
    if len(password) < min_len:
        set_errors.append(_('Password is too short.  It must be at least %d characters.') % min_len)
    max_len = passwords_cfg.get('max_length', 0)
    if max_len and len(password) > max_len:
        set_errors.append(_('Password is too long.  It must be at most %d characters.') % max_len)

    if passwords_cfg.get('count_uppercase'):
        count = int(passwords_cfg.get('count_uppercase'))
        if len(filter(lambda c: c.isupper(), password)) < count:
            set_errors.append(
                ngettext('Password must contain an uppercase character.',
                    'Password must contain at least %(count)d uppercase characters.',
                    count) % {'count': count})

    if passwords_cfg.get('count_lowercase'):
        count = int(passwords_cfg.get('count_lowercase'))
        if len(filter(lambda c: c.islower(), password)) < count:
            set_errors.append(
                ngettext('Password must contain a lowercase character.',
                    'Password must contain at least %(count)d lowercase characters.',
                    count) % {'count': count})

    if passwords_cfg.get('count_digit'):
        count = int(passwords_cfg.get('count_digit'))
        if len(filter(lambda c: c.isdigit(), password)) < count:
            set_errors.append(
                ngettext('Password must contain a digit.',
                    'Password must contain at least %(count)d digits.',
                    count) % {'count': count})

    if passwords_cfg.get('count_special'):
        count = int(passwords_cfg.get('count_special'))
        if len(filter(lambda c: not c.isalnum(), password)) < count:
            set_errors.append(
                ngettext('Password must contain a special character.',
                    'Password must contain at least %(count)d special characters.',
                    count) % {'count': count})

    if set_errors:
        form.set_error(widget_name, str(' ').join(set_errors))


class TokenDirectory(Directory):
    _q_exports = ['']

    def __init__(self, token):
        self.token = token

    def _q_index(self):
        try:
            self.token.remove_self()
        except OSError:
            # race condition, and the token already got removed (??!)
            self.token.type = None

        r = TemplateIO(html=True)
        if self.token.type is None:
            template.html_top(_('Invalid Token'))
            r += TextsDirectory.get_html_text('invalid-password-token')

        elif self.token.type == 'account-confirmation':
            template.html_top(_('Account Creation Confirmed'))
            account = PasswordAccount.get(self.token.username)
            account.awaiting_confirmation = False
            account.store()

            if account.awaiting_moderation:
                r += TextsDirectory.get_html_text('account-created-waiting-activation')
            else:
                r += TextsDirectory.get_html_text('account-created')
                passwords_cfg = get_cfg('passwords', {})
                if passwords_cfg.get('can_change', False):
                    # TODO: offer a chance to change password ?
                    pass

            identities_cfg = get_cfg('identities', {})
            if identities_cfg.get('notify-on-register', False):
                notify_admins_user_registered(account)

        else:
            raise errors.TraversalError()

        return r.getvalue()


class TokensDirectory(Directory):
    def _q_lookup(self, component):
        try:
            token = tokens.Token.get(component)
        except KeyError:
            raise errors.TraversalError()
        return TokenDirectory(token)


class MethodDirectory(Directory):
    _q_exports = ['login', 'register', 'tokens', 'forgotten']

    tokens = TokensDirectory()

    def _q_traverse(self, path):
        get_response().breadcrumb.append(('password/', None))
        return Directory._q_traverse(self, path)

    def login(self):
        next_url = get_request().form.get('next')
        if get_request().get_method() == 'GET':
            get_request().form = {}
        identities_cfg = get_cfg('identities', {})
        form = Form(enctype='multipart/form-data', id='login-form',
                use_tokens=False, action=get_request().get_url())
        form.add_hidden('next', next_url)
        if identities_cfg.get('email-as-username', False):
            form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
        else:
            form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
        form.add(PasswordWidget, 'password', title = _('Password'), size=25, required=True)
        form.add_submit('submit', _('Log in'))
        if form.is_submitted() and not form.has_errors():
            tmp = self.login_submit(form)
            if not form.has_errors():
                return tmp

        get_response().breadcrumb.append(('login', _('Login')))
        template.html_top(_('Login'))
        r = TemplateIO(html=True)
        r += htmltext('<div class="ident-content">')
        r += htmltext('<div id="login">')
        r += get_session().display_message()
        r += TextsDirectory.get_html_text('top-of-login')
        r += form.render()
        r += htmltext('</div>')

        if identities_cfg.get('creation') in ('self', 'moderated'):
            r += htmltext('<div id="register">')
            ident_methods = get_cfg('identification', {}).get('methods', [])
            if len(ident_methods) > 1:
                register_url = get_publisher().get_root_url() + 'register/password/register'
            else:
                register_url = get_publisher().get_root_url() + 'register/'
            r += TextsDirectory.get_html_text(str('password-account-link-to-register-page'),
                    {'register_url': register_url})
            r += htmltext('</div>')

        forgotten_url = get_publisher().get_root_url() + 'ident/password/forgotten'
        r += htmltext('<div id="forgotten">')
        r += htmltext('<h3>%s</h3>') % _('Lost Password?')
        r += TextsDirectory.get_html_text(str('password-forgotten-link')) % {
                'forgotten_url': forgotten_url}
        r += htmltext('</div>')
        r += htmltext('</div>') # .ident-content

        r += htmltext("""<script type="text/javascript">
          document.getElementById('login-form')['username'].focus();
        </script>""")
        return r.getvalue()

    def login_submit(self, form):
        username = form.get_widget('username').parse()
        password = form.get_widget('password').parse()

        try:
            user = PasswordAccount.get_with_credentials(username, password)
        except:
            form.set_error('username', _('Invalid credentials'))
            return

        account = PasswordAccount.get(username)
        return self.login_submit_account_user(account, user, form)

    def login_submit_account_user(self, account, user, form=None):

        if account.awaiting_moderation:
            if form:
                form.set_error('username', _('This account is waiting for moderation'))
            return

        if account.awaiting_confirmation:
            if form:
                form.set_error('username', _('This account is waiting for confirmation'))
            return

        if account.disabled:
            if form:
                form.set_error('username', _('This account has been disabled'))
            return

        session = get_session()
        session.username = account.id
        session.set_user(user.id)

        if account.warned_about_unused_account:
            account.warned_about_unused_account = False
            account.store()

        if form and form.get_widget('next').parse():
            after_url = form.get_widget('next').parse()
            return redirect(after_url)
        else:
            return redirect(get_publisher().get_root_url() + get_publisher().after_login_url)


    def forgotten(self, include_mode = False):
        if 't' in get_request().form:
            return self.forgotten_token()

        identities_cfg = get_cfg('identities', {})

        if include_mode:
            base_url = get_publisher().get_root_url() + 'ident/password'
            form = Form(enctype='multipart/form-data', use_tokens=False,
                            action='%s/forgotten' % base_url)
        else:
            form = Form(enctype='multipart/form-data', use_tokens=False)

        if identities_cfg.get('email-as-username', False):
            form.add(StringWidget, 'username', title = _('Email'), size=25, required=True)
        else:
            form.add(StringWidget, 'username', title = _('Username'), size=25, required=True)
        form.add_submit('change', _('Submit Request'))

        if include_mode:
            form.clear_errors()
        if not include_mode and form.is_submitted() and not form.has_errors():
            tmp = self.forgotten_submit(form)
            if not form.has_errors() and tmp:
                return tmp

        r = TemplateIO(html=True)
        if not include_mode:
            get_response().breadcrumb.append(('forgotten', _('Forgotten password')))
            template.html_top(_('Forgotten password'))
            r += htmltext('<div class="ident-content">')

        r += TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
        r += form.render()
        if not include_mode:
            r += htmltext('</div>')
        return r.getvalue()


    def forgotten_submit(self, form):
        username = form.get_widget('username').parse()

        try:
            account = PasswordAccount.get(username)
            user = account.user
        except KeyError:
            user = None

        if not user or user.email is None:
            form.set_error('username',
                    _('There is no user with that name or it has no email contact.'))
            return None

        # changing password, process:
        #  - sending mail with a token (sth like http://.../forgotten?token=xxx)
        #  - enter your new password

        token = tokens.Token(3 * 86400)
        token.type = 'password-change'
        token.username = username
        token.store()

        data = {
            'change_url': get_request().get_frontoffice_url() + '?t=%s&a=cfmpw' % token.id,
            'cancel_url': get_request().get_frontoffice_url() + '?t=%s&a=cxlpw' % token.id,
            'token': token.id,
            'time': misc.localstrftime(time.localtime(token.expiration)),
        }

        try:
            emails.custom_template_email('change-password-request', data,
                    user.email, exclude_current_user = False)
        except errors.EmailError:
            form.set_error('username', _('Failed to send email (server error)'))
            token.remove_self()
            return None

        def forgotten_token_sent():
            template.html_top(_('Forgotten Password'))
            r = TemplateIO(html=True)
            r += htmltext('<div class="ident-content">')
            r += TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
            r += htmltext('</div>')
            return r.getvalue()

        return forgotten_token_sent()


    def forgotten_token(self):
        tokenv = get_request().form.get('t')
        action = get_request().form.get('a')

        try:
            token = tokens.Token.get(tokenv)
        except KeyError:
            return template.error_page(
                    _('The token you submitted does not exist, has expired, or has been cancelled.'),
                    continue_to = (get_publisher().get_root_url(), _('home page')))

        if token.type != 'password-change':
            return template.error_page(
                    _('The token you submitted is not appropriate for the requested task.'),
                    continue_to = (get_publisher().get_root_url(), _('home page')))

        if action == 'cxlpw':
            template.html_top(_('Password Change'))
            r = TemplateIO(html=True)
            r += htmltext('<div class="ident-content">')
            r += htmltext('<h1>%s</h1>') % _('Request Cancelled')
            r += htmltext('<p>%s</p>') % _('Your request has been cancelled')
            r += htmltext('<p>')
            r += htmltext(_('Continue to <a href="/">home page</a></p>'))
            r += htmltext('</p>')
            r += htmltext('</div>')
            token.remove_self()
            return r.getvalue()

        passwords_cfg = get_cfg('passwords', {})
        if action == 'cfmpw' and passwords_cfg.get('can_change', False):
            form = Form(enctype='multipart/form-data', action='forgotten')
            form.add(HiddenWidget, 't', value = tokenv)
            form.add(HiddenWidget, 'a', value = action)
            form.add(PasswordEntryWidget, 'new_password', title=_('New Password'),
                    required=True, formats=['cleartext'],
                    **get_cfg('passwords', {}))
            form.add_submit('submit', _('Submit'))
            form.add_submit('cancel', _('Cancel'))

            if form.get_submit() == 'cancel':
                token.remove_self()
                return redirect('.')

            if form.is_submitted() and not form.has_errors():
                new_password = form.get_widget('new_password').parse().get('cleartext')

            if form.is_submitted() and not form.has_errors():
                account = PasswordAccount.get(token.username)
                account.hashing_algo = passwords_cfg.get('hashing_algo')
                account.set_password(new_password)
                account.store()
                token.remove_self()
                user = PasswordAccount.get_with_credentials(account.id, new_password)
                tmp = self.login_submit_account_user(account, user)
                if tmp:
                    return tmp
                return redirect('login/')

            template.html_top(_('Password Change'))
            get_request().form = {}
            return form.render()

        if action == 'cfmpw' and not passwords_cfg.get('can_change', False):
            # generate a new password and send it by email
            new_password = make_password()
            try:
                account = PasswordAccount.get(token.username)
                user = account.user
            except KeyError:
                user = None

            account.hashing_algo = passwords_cfg.get('hashing_algo')
            account.set_password(str(new_password))
            account.store()
            token.remove_self()

            if user and user.email:
                data = {
                    'username': str(account.id),
                    'password': str(new_password),
                    'hostname': get_request().get_server(),
                }

                emails.custom_template_email('new-generated-password', data,
                        user.email, exclude_current_user = False)

                return self.forgotten_token_end_page()
            else:
                pass # XXX: user has no email, what to tell him ?

            return redirect('login/')

    def forgotten_token_end_page(self):
        r = TemplateIO(html=True)
        r += htmltext('<div class="ident-content">')
        r += template.html_top(_('New password sent by email'))
        r += htmltext('</div>')
        return r.getvalue()

    def register(self):
        identities_cfg = get_cfg('identities', {})
        if identities_cfg.get('creation', 'admin') == 'admin':
            raise errors.TraversalError()

        passwords_cfg = get_cfg('passwords', {})
        identities_cfg = get_cfg('identities', {})
        users_cfg = get_cfg('users', {})

        form = Form(enctype = 'multipart/form-data', use_tokens = False)

        formdef = None
        if hasattr(get_publisher().user_class, str('get_formdef')):
            formdef = get_publisher().user_class.get_formdef()
            if formdef:
                formdef.add_fields_to_form(form)

        if not identities_cfg.get('email-as-username', False):
            form.add(StringWidget, 'username', title = _('Username'), size=25, required=True,
                    hint=_('This will be your username to connect to this site.'))
        else:
            if not users_cfg.get('field_email'):
                form.add(EmailWidget, 'username', title = _('Email'), size=25, required=True)

        r = TemplateIO(html=True)

        if not passwords_cfg.get('generate', True):
            form.add(PasswordEntryWidget, 'password', title=_('Password'),
                    size=25, required=True,
                    formats=['cleartext'],
                    **get_cfg('passwords', {}))

        form.add_submit('submit', _('Create Account'))

        if form.is_submitted() and not form.has_errors():
            tmp = self.register_submit(form, formdef)
            if not form.has_errors():
                return tmp

        get_response().breadcrumb.append(('register', _('New Account')))
        template.html_top(_('New Account'))
        r += htmltext('<div class="ident-content">')
        r += TextsDirectory.get_html_text('new-account')
        r += form.render()
        r += htmltext('</div>')
        return r.getvalue()

    def register_submit(self, form, formdef):
        passwords_cfg = get_cfg('passwords', {})
        identities_cfg = get_cfg('identities', {})
        users_cfg = get_cfg('users', {})

        if not identities_cfg.get('email-as-username', False) or \
                not users_cfg.get('field_email'):
            username = form.get_widget('username').parse()
            username_field_key = 'username'
        else:
            data = formdef.get_data(form)
            username = data.get(users_cfg.get('field_email'))
            username_field_key = 'f%s' % users_cfg.get('field_email')

        if PasswordAccount.has_key(username):
            if username_field_key == 'username':
                form.set_error(username_field_key,
                        _('There is already a user with that username'))
            else:
                form.set_error(username_field_key,
                        _('There is already a user with that email address'))

        if form.has_errors():
            return

        password = None
        if passwords_cfg.get('generate', True):
            password = make_password()
            # an email will be sent afterwards
        else:
            password = form.get_widget('password').parse().get('cleartext')

        user = get_publisher().user_class()
        user.name = username
        if formdef:
            data = formdef.get_data(form)
            if identities_cfg.get('email-as-username', False) and not 'email' in data:
                data['email'] = username
            user.set_attributes_from_formdata(data)
            user.form_data = data
        else:
            if identities_cfg.get('email-as-username', False):
                user.email = username

        if get_publisher().user_class.count() == 0:
            user.is_admin = True
        user.store()

        account = PasswordAccount(id = username)
        account.hashing_algo = passwords_cfg.get('hashing_algo')
        if password:
            account.set_password(password)
        account.user_id = user.id
        if identities_cfg.get('creation') == 'moderated':
            account.awaiting_moderation = True

        if identities_cfg.get('email-confirmation', False):
            if not user.email:
                get_logger().error(_('Accounts are configured to require confirmation but accounts can be created without emails'))
            else:
                account.awaiting_confirmation = True

        account.store()

        if account.awaiting_confirmation:
            return self.confirmation_notification(account, user, password)

        if identities_cfg.get('notify-on-register', False):
            notify_admins_user_registered(account)

        if account.awaiting_moderation:
            return self.moderation_notification()

        if passwords_cfg.get('generate', True):
            if not user.email:
                get_logger().error(
                        _('Accounts are configured to have a generated password '
                          'but accounts can be created without emails'))
            else:
                data = {
                    'hostname': get_request().get_server(),
                    'email': user.email,
                    'email_as_username': str(identities_cfg.get('email-as-username', False)),
                    'username': account.id,
                    'password': password,
                }
                emails.custom_template_email('new-account-generated-password', data,
                            user.email, fire_and_forget = True)

        # XXX: display a message instead of immediate redirect ?
        return redirect(get_publisher().get_root_url() + 'login/')

    def moderation_notification(self):
        template.html_top(_('Account created, waiting for moderation'))
        r = TemplateIO(html=True)

        r += htmltext('<div class="ident-content">')
        r += htmltext('<p>')
        r += _('A site administrator will now review then activate your account.')
        r += htmltext('</p>')

        r += htmltext('<p>')
        r += _('You will then get your password by email.')
        r += htmltext('</p>')

        r += htmltext('<p>')
        r += htmltext('<a href="%s">%s</a>') % (get_publisher().get_root_url(), _('Back to home page'))
        r += htmltext('</p>')
        r += htmltext('</div>')

        return r.getvalue()

    def confirmation_notification(self, account, user, password):
        self.email_confirmation_notification(account, user, password)

        template.html_top(_('Email sent'))
        r = TemplateIO(html=True)
        r += htmltext('<div class="ident-content">')
        r += TextsDirectory.get_html_text('email-sent-confirm-creation')
        r += htmltext('</div>')
        return r.getvalue()

    def email_confirmation_notification(self, account, user, password):
        passwords_cfg = get_cfg('passwords', {})

        token = tokens.Token(3 * 86400)
        token.type = 'account-confirmation'
        token.username = account.id
        token.store()

        req = get_request()
        path = get_publisher().get_root_url() + 'ident/password/tokens/%s/' % token.id
        token_url = '%s://%s%s' % (req.get_scheme(), req.get_server(), path)

        data = {
            'email': user.email,
            'website': get_cfg('sitename'),
            'token_url': token_url,
            'token': token.id,
            'username': account.id,
            'password': password,
            'admin_email': passwords_cfg.get('admin_email', ''),
        }

        emails.custom_template_email('password-subscription-notification', data, user.email)

ADMIN_TITLE = N_('Username / Password')

class MethodAdminDirectory(Directory):
    title = ADMIN_TITLE
    label = N_('Configure username/password identification method')

    _q_exports = ['', 'passwords', 'identities', ('import', 'p_import')]

    def _q_index(self):
        html_top('settings', title = _(ADMIN_TITLE))
        get_response().breadcrumb.append( ('password/', _(self.title)))
        r = TemplateIO(html=True)

        r += htmltext('<h2>%s</h2>') % _(ADMIN_TITLE)

        r += get_session().display_message()

        r += htmltext('<dl>')
        r += htmltext('<dt><a href="identities">%s</a></dt> <dd>%s</dd>') % (
                _('Identities'), _('Configure identities creation'))
        r += htmltext('<dt><a href="passwords">%s</a></dt> <dd>%s</dd>') % (
                _('Passwords'), _('Configure all password things'))
        r += htmltext('<dt><a href="import">%s</a></dt> <dd>%s</dd>') % (
                _('Bulk Import'), _('Import accounts from a CSV file'))
        r += htmltext('</dl>')
        return r.getvalue()

    def passwords(self):
        form = Form(enctype='multipart/form-data')
        passwords_cfg = get_cfg('passwords', {})
        form.add(CheckboxWidget, 'can_change', title = _('Users can change their password'),
                value = passwords_cfg.get('can_change', False))
        form.add(CheckboxWidget, 'generate', title = _('Generate initial password'),
                value = passwords_cfg.get('generate', True))
        form.add(IntWidget, 'min_length', title = _('Minimum password length'),
                value = int(passwords_cfg.get('min_length', 0)))
        form.add(IntWidget, 'max_length', title = _('Maximum password length'),
                value = int(passwords_cfg.get('max_length', 0)),
                hint = _('0 for unlimited length'))
        form.add(IntWidget, 'count_uppercase',
                title = _('Minimum number of uppercase characters'),
                value = int(passwords_cfg.get('count_uppercase', 0)))
        form.add(IntWidget, 'count_lowercase',
                title = _('Minimum number of lowercase characters'),
                value = int(passwords_cfg.get('count_lowercase', 0)))
        form.add(IntWidget, 'count_digit',
                title = _('Minimum number of digits'),
                value = int(passwords_cfg.get('count_digit', 0)))
        form.add(IntWidget, 'count_special',
                title = _('Minimum number of special characters'),
                value = int(passwords_cfg.get('count_special', 0)))
        form.add(EmailWidget, 'admin_email', title = _('Email address (for questions...)'),
                value = passwords_cfg.get('admin_email'))
        hashing_options = [(None, _('None'))]
        for key in sorted(HASHING_ALGOS.keys()):
            hashing_options.append((key, key.upper()))
        form.add(SingleSelectWidget, 'hashing_algo',
                title = _('Password Hashing Algorithm'),
                value = passwords_cfg.get('hashing_algo'),
                options = hashing_options)

        form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))
        if form.get_submit() == 'cancel':
            return redirect('.')

        if form.is_submitted() and not form.has_errors():
            self.passwords_submit(form)
            return redirect('.')

        html_top('settings', title = _('Passwords'))
        r = TemplateIO(html=True)
        r += htmltext('<h2>%s</h2>') % _('Passwords')
        r += form.render()
        return r.getvalue()

    def passwords_submit(self, form):
        from wcs.admin.settings import cfg_submit
        cfg_submit(form, 'passwords',
                ('can_change', 'generate',
                'min_length', 'max_length',
                'count_uppercase', 'count_lowercase', 'count_digit', 'count_special',
                'admin_email', 'hashing_algo'))

    def identities(self):
        form = Form(enctype='multipart/form-data')
        identities_cfg = get_cfg('identities', {})
        form.add(SingleSelectWidget, 'creation', title = _('Identity Creation'),
                value = identities_cfg.get('creation', 'admin'),
                options = [(str('admin'), _('Site Administrator')),
                            (str('self'), _('Self-registration')),
                            (str('moderated'), _('Moderated user registration'))])
        form.add(CheckboxWidget, 'email-confirmation',
                title = _('Require email confirmation for new accounts'),
                value = identities_cfg.get('email-confirmation', False))
        form.add(CheckboxWidget, 'notify-on-register',
                title = _('Notify Administrators on Registration'),
                value = identities_cfg.get('notify-on-register', False))
        form.add(CheckboxWidget, 'email-as-username', title = _('Use email as username'),
                value = identities_cfg.get('email-as-username', False))
        form.add(IntWidget, 'warn_about_unused_account_delay',
                title = _('Warn about unused account after so many days'),
                hint = _('0 for no warning'),
                value = int(identities_cfg.get('warn_about_unused_account_delay', 0)))
        form.add(IntWidget, 'remove_unused_account_delay',
                title = _('Removed unused account after so many days'),
                hint = _('0 for no automatic removal'),
                value = int(identities_cfg.get('remove_unused_account_delay', 0)))

        if identities_cfg.get('locked') is None:
            form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))

        if form.get_submit() == 'cancel':
            return redirect('.')

        if form.is_submitted() and not form.has_errors():
            self.identities_submit(form)
            return redirect('.')

        html_top('settings', title = _('Identities Interface'))
        r = TemplateIO(html=True)
        r += htmltext('<h2>%s</h2>') % _('Identities Interface')
        r += form.render()
        return r.getvalue()

    def identities_submit(self, form):
        from wcs.admin.settings import cfg_submit
        cfg_submit(form, 'identities',
                ('creation', 'email-as-username', 'notify-on-register', 'email-confirmation',
                 'warn_about_unused_account_delay', 'remove_unused_account_delay'))

    def p_import(self):
        if get_request().form.get('job'):
            return self.sending_notification()

        passwords_cfg = get_cfg('passwords', {})

        form = Form(enctype='multipart/form-data')
        form.add(FileWidget, 'file', title = _('File'), required = True)
        form.add(CheckboxWidget, 'send_notifications',
                        title=_('Send notifications to users'),
                        required=False,
                        value=False)
        user_class = get_publisher().user_class()
        if hasattr(user_class, str('get_available_roles')):
            roles = user_class.get_available_roles()
            if roles:
                roles = [(None, '---', '')] + roles
                form.add(WidgetList, 'roles', title=_('Roles'),
                                element_type=SingleSelectWidget,
                                add_element_label=_('Add Role'),
                                element_kwargs={
                                        str('render_br'): False,
                                        str('options'): roles})

        form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))

        if form.get_submit() == 'cancel':
            return redirect('.')

        if form.is_submitted() and not form.has_errors():
            t = self.import_submit(form)
            if not form.has_errors():
                if t:
                    return t
                return redirect('.')

        html_top('settings', title = _('Bulk Import'))
        r = TemplateIO(html=True)
        identities_cfg = get_cfg('identities', {})
        users_cfg = get_cfg('users', {})
        r += htmltext('<h2>%s</h2>') % _('Bulk Import')
        r += htmltext('<p>')
        r += _('The CSV file must strictly adhere to the following structure:')
        r += htmltext('</p>')
        r += htmltext('<ul>')
        r += htmltext('<li>%s</li>') % _('Charset: %s') % get_publisher().site_charset
        r += htmltext('<li>%s</li>') % _('Column Separator: ;')
        r += htmltext('<li>%s') % _('Columns:')
        r += htmltext('<ul>')
        if not identities_cfg.get('email-as-username', False):
            r += htmltext('<li>%s</li>') % _('Username')
        formdef = get_publisher().user_class.get_formdef()
        if not formdef or not users_cfg.get('field_name'):
            r += htmltext('<li>%s</li>') % _('Name')
        if not formdef or not users_cfg.get('field_email'):
            r += htmltext('<li>%s</li>') % _('Email')
        r += htmltext('<li>%s') % _('Password')
        if passwords_cfg.get('hashing_algo'):
            r += ' '
            r += _('(%s hash)') % passwords_cfg.get('hashing_algo').upper()
        r += ' '
        r += _('(empty to get an automatically generated password)')
        r += htmltext('</li>')
        if formdef:
            for field in formdef.fields:
                r += htmltext('<li>%s</li>') % field.label
        r += htmltext('</ul>')
        r += htmltext('</li>')
        r += htmltext('</li>')
        r += htmltext('</ul>')

        r += form.render()
        return r.getvalue()

    def import_submit(self, form):
        identities_cfg = get_cfg('identities', {})
        passwords_cfg = get_cfg('passwords', {})
        users_cfg = get_cfg('users', {})

        hashing_algo = passwords_cfg.get('hashing_algo')

        required_nb_columns = 0
        if not identities_cfg.get('email-as-username', False):
            required_nb_columns += 1
            username_field_number = 0
        formdef = get_publisher().user_class.get_formdef()
        if not formdef or not users_cfg.get('field_name'):
            name_field = required_nb_columns
            required_nb_columns += 1
        if not formdef or not users_cfg.get('field_email'):
            email_field = required_nb_columns
            if identities_cfg.get('email-as-username', False):
                username_field_number = required_nb_columns
            required_nb_columns += 1
        password_field_number = required_nb_columns
        required_nb_columns += 1
        if formdef:
            base_formdef_field_no = required_nb_columns
            required_nb_columns += len(formdef.fields)

        send_notifications = form.get_widget('send_notifications').parse()
        created_accounts = []

        roles = None
        if form.get_widget('roles'):
            roles = form.get_widget('roles').parse()

        objects = []
        reader = csv.reader(form.get_widget('file').parse().fp, delimiter=';')
        for i, csv_line in enumerate(reader):
            if len(csv_line) != required_nb_columns:
                form.set_error('file', _('Incorrect number of columns (line: %s)') % (i+1))
                return

            u = get_publisher().user_class()
            if formdef:
                if not users_cfg.get('field_name'):
                    u.name = csv_line[name_field]
                if not users_cfg.get('field_email'):
                    u.email = csv_line[email_field]
                data = {}
                for j, field in enumerate(formdef.fields):
                    if identities_cfg.get('email-as-username', False) and \
                            users_cfg.get('field_email') == field.id:
                        username_field_number = base_formdef_field_no+j
                    data[field.id] = csv_line[base_formdef_field_no+j]
                u.set_attributes_from_formdata(data)
                u.form_data = data
            else:
                u.name = csv_line[name_field]
                u.email = csv_line[email_field]

            if roles:
                u.add_roles(roles)

            username = csv_line[username_field_number]
            if PasswordAccount.has_key(username):
                form.set_error('file', _('Duplicate username (line: %s)') % (i+1))
                return
            if [x for x in objects if x[1].id == username]:
                form.set_error('file', _('Duplicate username (line: %s)') % (i+1))
                return

            password = csv_line[password_field_number]

            p = PasswordAccount(id=username)
            if hashing_algo and password:
                p.hashing_algo = hashing_algo
                p.password = password
                password = None
            elif password:
                p.hashing_algo = hashing_algo
                p.set_password(password)

            objects.append((u, p))

            if send_notifications:
                if not p.password:
                    password = make_password()
                    p.set_password(password)
                created_accounts.append({'username': username,
                                'email_as_username': identities_cfg.get('email-as-username', False),
                                'hostname': get_request().get_server(),
                                'email': u.email,
                                'password': password})

        for u, p in objects:
            u.id = u.get_new_id()
            u.store()
            p.user_id = u.id
            p.store()

        get_session().message = ('info', _('Number of accounts created: %s') % (i+1))

        if send_notifications:
            class NotificationSender(object):
                def __init__(self, accounts):
                    self.accounts = accounts
                def email(self, job=None):
                    for account in self.accounts:
                        if not account.get('email'):
                            continue
                        emails.custom_template_email('new-account-generated-password',
                                        account, account['email'],
                                        fire_and_forget=False)

            job = get_response().add_after_job(
                    str(N_('Sending subscription emails')),
                    NotificationSender(created_accounts).email)
            return redirect('import?job=%s' % job.id)

    def sending_notification(self):
        try:
            job = AfterJob.get(get_request().form.get('job'))
        except KeyError:
            return redirect('..')
        html_top('settings', title=_('Notifications'))
        r = TemplateIO(html=True)
        r += get_session().display_message()
        get_response().add_javascript(['jquery.js', 'afterjob.js'])
        r += htmltext('<dl class="job-status">')
        r += htmltext('<dt>')
        r += _(job.label)
        r += htmltext('</dt>')
        r += htmltext('<dd>')
        r += htmltext('<span class="afterjob" id="%s">') % job.id
        r += _(job.status)
        r += htmltext('</span>')
        r += htmltext('</dd>')
        r += htmltext('</dl>')

        r += htmltext('<div class="done">')
        r += htmltext('<a href="./">%s</a>') % _('Back')
        r += htmltext('</div>')
        return r.getvalue()


class UsernamePasswordWidget(CompositeWidget):
    def __init__(self, name, value=None, **kwargs):
        CompositeWidget.__init__(self, name, value, **kwargs)
        if not value:
            value = {}

        self.add(StringWidget, 'username', value.get('username'), title = _('Username'),
                required = kwargs.get('required'))

        if value.get('password'):
            kwargs['required'] = False
        self.add(PasswordWidget, 'password', title = _('Password'),
                required = kwargs.get('required'),
                autocomplete = 'off')
        self.add(CheckboxWidget, 'awaiting_confirmation', value.get('awaiting_confirmation'),
                title = _('Awaiting Confirmation'), required = False)
        self.add(CheckboxWidget, 'awaiting_moderation', value.get('awaiting_moderation'),
                title = _('Awaiting Moderation'), required = False)
        self.add(CheckboxWidget, 'disabled', value.get('disabled'),
                title = _('Disabled Account'), required = False)

    def _parse(self, request):
        value = {
            'username': self.get('username'),
            'password': self.get('password'),
            'awaiting_moderation': self.get('awaiting_moderation'),
            'awaiting_confirmation': self.get('awaiting_confirmation'),
            'disabled': self.get('disabled'),
        }
        self.value = value or None


class MethodUserDirectory(Directory):
    _q_exports = ['email']

    def __init__(self, user):
        self.user = user
        try:
            self.account = PasswordAccount.get_on_index(user.id, 'user_id')
        except KeyError:
            raise NoSuchMethodForUserError()

    def get_actions(self):
        actions = []
        if self.account.hashing_algo:
            actions.append(('email', _('Send new password by email')))
        else:
            actions.append(('email', _('Send password by email')))

        return actions

    def email(self):
        html_top('users', title=_(ADMIN_TITLE))
        r = TemplateIO(html=True)
        get_response().breadcrumb.append(('email', 'Email Password'))
        r += htmltext('<h2>%s</h2>') % _('Email Password')
        form = Form(enctype='multipart/form-data')
        options = [('create-anew', _('Generate new password'))]
        if not self.account.hashing_algo:
            options.append(('current', _('Use current password')))
        # TODO: option to send a mail with a token url, asking user to enter a
        # new password

        form.add(RadiobuttonsWidget, 'method', options=options, delim='<br/>',
                 required=True)

        form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))

        if form.get_submit() == 'cancel':
            return redirect('..')

        if form.is_submitted() and not form.has_errors():
            self.email_submit(form)
            return redirect('..')

        r += form.render()
        return r.getvalue()

    def email_submit(self, form):
        method = form.get_widget('method').parse()
        email_key = 'password-email-%s' % method
        if method == 'create-anew':
            password = make_password()
            self.account.set_password(password)
            self.account.store()
        else:
            password = self.account.password

        data = {
            'hostname': get_request().get_server(),
            'name': self.user.get_display_name(),
            'username': self.account.id,
            'password': password
        }

        emails.custom_template_email(email_key, data, self.user.email)

class PasswordAuthMethod(AuthMethod):
    key = 'password'
    description = _('Username / password')
    method_directory = MethodDirectory
    method_admin_widget = UsernamePasswordWidget
    method_admin_directory = MethodAdminDirectory
    method_user_directory = MethodUserDirectory

    def submit(self, user, widget):
        passwords_cfg = get_cfg('passwords', {})
        value = widget.parse()
        username = value.get('username')
        if not username:
            return
        if PasswordAccount.has_key(username):
            account = PasswordAccount.get(username)
            if account.user_id != user.id:
                widget.value = None
                widget.set_widget_error('username', _('Duplicate user name'))
                return
        else:
            account = PasswordAccount(id = value.get('username'))
        if value.get('password'):
            account.hashing_algo = passwords_cfg.get('hashing_algo')
            account.set_password(value.get('password'))
        account.awaiting_confirmation = value.get('awaiting_confirmation')
        account.awaiting_moderation = value.get('awaiting_moderation')
        account.disabled = value.get('disabled')
        account.user_id = user.id
        try:
            old_account = PasswordAccount.get_on_index(user.id, 'user_id')
        except KeyError:
            pass
        else:
            if old_account.id != account.id:
                old_account.remove_self()
        account.store()

    def delete(self, user):
        try:
            old_account = PasswordAccount.get_on_index(user.id, 'user_id')
            old_account.remove_self()
        except KeyError:
            pass

    def get_value(self, user):
        if not user or not user.id:
            return None
        try:
            account = PasswordAccount.get_on_index(user.id, 'user_id')
        except KeyError:
            return None
        return {'username': account.id, 'password': account.password,
                'awaiting_moderation': account.awaiting_moderation,
                'awaiting_confirmation': account.awaiting_confirmation,
                'disabled': account.disabled,
                }

    @classmethod
    def register(cls):
        rdb = get_publisher_class().backoffice_directory_class
        if rdb:
            rdb.register_directory('accounts', AccountsDirectory())

            def menu_entry_check_display(k):
                identities_cfg = get_cfg('identities', {})
                if identities_cfg.get('creation') != 'moderated':
                    return False
                user = get_request().user
                if not user:
                    return False
                if not user.is_admin:
                    return False
                return True

            rdb.register_menu_item('accounts/', _('Accounts'),
                    check_display_function=menu_entry_check_display)


class AccountDirectory(Directory):
    _q_exports = ['', 'accept', 'reject', 'email']

    def __init__(self, account):
        self.account = account

    def _q_traverse(self, path):
        get_response().breadcrumb.append(('%s/' % self.account.id, self.account.id))
        return Directory._q_traverse(self, path)

    def _q_index(self):
        identities_cfg = get_cfg('identities', {})
        users_cfg = get_cfg('users', {})

        html_top('accounts', _('Account - %s') % self.account.id)
        r = TemplateIO(html=True)
        r += htmltext('<h2>%s</h2>') % _('Moderation of account')

        r += htmltext('<div class="dataview">')

        if not identities_cfg.get('email-as-username', False):
            r += htmltext('<div class="field"><span class="label">%s</span>') % _('Username')
            r += htmltext('<span class="value">%s</span></div>') % self.account.id
        else:
            if not users_cfg.get('field_email'):
                r += htmltext('<div class="field"><span class="label">%s</span>') % _('Email')
                r += htmltext('<span class="value">%s</span></div>') % self.account.id

        formdef = get_publisher().user_class.get_formdef()
        if formdef:
            data = self.account.user.form_data
            for f in formdef.fields:
                if f.id not in data:
                    continue
                value = data[f.id]
                if value is None or value == '':
                    continue

                r += htmltext('<div class="field"><span class="label">%s</span> ') % f.label
                r += htmltext('<div class="value">')
                s = f.get_view_value(value)
                r += htmltext(s)
                r += htmltext('</div></div>')
        r += htmltext('</div>')

        if self.account.user.email:
            r += htmltext('<a href="email">%s</a> - ') % _('Reply by email')
        r += htmltext('<a href="accept">%s</a>') % _('Accept')
        r += ' - '
        r += htmltext('<a href="reject">%s</a>') % _('Reject')
        return r.getvalue()

    def accept(self):
        passwords_cfg = get_cfg('passwords', {})
        if passwords_cfg.get('generate', True) and self.account.hashing_algo:
            # generated password, as it may be hashed, create it now
            password = make_password()
            self.account.set_password(str(password))
        elif self.account.hashing_algo:
            password = None
        else:
            password = self.account.password

        self.account.awaiting_moderation = False
        self.account.store()

        try:
            user = self.account.user
        except KeyError:
            user = None

        if not user or not user.email:
            return redirect('..')

        data = {
            'username': self.account.id,
        }
        if password:
            data['password'] = password

        emails.custom_template_email('new-account-approved', data, user.email)

        return redirect('..')

    def reject(self):
        form = Form(enctype = 'multipart/form-data')
        form.add(EmailWidget, 'to', title = _('To'), value = self.account.user.email)
        form.add(StringWidget, 'subject', title = _('Subject'))
        form.add(TextWidget, 'body', title = _('Message'), cols = 80, rows = 15)
        form.add_submit('submit', _('Submit'))
        form.add_submit('submit-no-msg', _("Submit and don't send email"))
        form.add_submit('cancel', _('Cancel'))

        if form.get_submit() == 'cancel':
            return redirect('.')

        if not form.is_submitted():
            get_response().breadcrumb.append(('reject', _('Rejection')))
            html_top('accounts', _('Rejection'))
            return form.render()
        else:
            self.account.user.remove_self()
            self.account.remove_self()
            if form.get_submit() != 'submit-no-msg':
                mail_subject = form.get_widget('subject').parse()
                mail_body = form.get_widget('body').parse()
                to = form.get_widget('to').parse()
                emails.email(mail_subject, mail_body, to)
            return redirect('..')

    def email(self):
        form = Form(enctype = 'multipart/form-data')
        form.add(EmailWidget, 'to', title = _('To'), value = self.account.user.email)
        form.add(StringWidget, 'subject', title = _('Subject'), size = 35,
                value = _('About your account request'))
        form.add(TextWidget, 'body', title = _('Message'), cols = 80, rows = 15)
        form.add_submit('submit', _('Submit'))
        form.add_submit('cancel', _('Cancel'))

        if form.get_submit() == 'cancel':
            return redirect('.')

        if not form.is_submitted():
            get_response().breadcrumb.append(('email', _('Reply by email')))
            html_top('accounts', _('Reply by email'))
            r = TemplateIO(html=True)
            r += htmltext('<h2>%s</h2>') % _('Reply by email')
            r += form.render()
            return r.getvalue()
        else:
            mail_subject = form.get_widget('subject').parse()
            mail_body = form.get_widget('body').parse()
            to = form.get_widget('to').parse()
            emails.email(mail_subject, mail_body, to)
            return redirect('.')



class AccountsDirectory(AccessControlled, Directory):
    _q_exports = ['']

    def _q_access(self):
        user = get_request().user
        if not user:
            raise errors.AccessUnauthorizedError()
        if not user.is_admin:
            raise errors.AccessForbiddenError(
                    public_msg = _('You are not allowed to access Accounts Management'),
                    location_hint = 'backoffice')

        get_response().breadcrumb.append(('accounts/', _('Accounts Management')))

    def _q_index(self):
        html_top('accounts', _('Accounts Management'))
        r = TemplateIO(html=True)
        r += htmltext('<h2>%s</h2>') % _('New accounts waiting for moderation')

        r += htmltext('<ul class="biglist">')
        for account in PasswordAccount.select(order_by = 'id'):
            if account.awaiting_confirmation:
                continue
            if not account.awaiting_moderation:
                continue
            if not account.user:
                # user has been removed; this is so wrong we remove account now
                account.remove_self()
                continue
            r += htmltext('<li>')
            r += htmltext('<strong class="label">%s</strong>') % account.user.display_name
            r += htmltext('<p class="details">')
            r += _('Username:')
            r += ' '
            r += account.id
            r += htmltext('</p>')

            r += htmltext('<p class="commands">')
            r += command_icon('%s/' % account.id, 'view')
            if account.user.email:
                r += command_icon('%s/email' % account.id, 'email', label = _('Reply by email'))
            r += command_icon('%s/accept' % account.id, 'accept',
                    label = _('Accept'), icon = 'stock_yes_16.png')
            r += command_icon('%s/reject' % account.id, 'reject',
                    label = _('Reject'), icon = 'stock_no_16.png', popup = True)
            r += htmltext('</p>')
            r += htmltext('</li>')
        r += htmltext('</ul>')
        return r.getvalue()

    def _q_lookup(self, component):
        try:
            account = PasswordAccount.get(component)
        except KeyError:
            return None
        return AccountDirectory(account)

EmailsDirectory.register('password-subscription-notification',
        N_('Subscription notification for password account'),
        N_('Available variables: email, website, token_url, token, admin_email, username, password'),
        category = N_('Identification'),
        default_subject = N_('Subscription Confirmation'),
        default_body = N_('''\
We have received a request for subscription of your email address,
"[email]", to the [website] web site.

To confirm that you want to be subscribed to the web site, simply
visit this web page:

[token_url]

If you do not wish to be subscribed to the web site, pleasy simply
disregard this message.  If you think you are being maliciously
subscribed to the web site, or have any other questions, send them
to [admin_email].
'''))

EmailsDirectory.register('change-password-request',
        N_('Request for password change'),
        N_('Available variables: change_url, cancel_url, token, time'),
        category = N_('Identification'),
        default_subject = N_('Change Password Request'),
        default_body = N_("""\
You have (or someone impersonating you has) requested to change your
password. To complete the change, visit the following link:

[change_url]

If you are not the person who made this request, or you wish to cancel
this request, visit the following link:

[cancel_url]

If you do nothing, the request will lapse after 3 days (precisely on
[time]).
"""))


EmailsDirectory.register('new-generated-password',
        N_('New generated password'),
        N_('Available variables: username, password, hostname'),
        category = N_('Identification'),
        default_subject = N_('Your new password'),
        default_body = N_('''\
Hello,

You have requested a new password for [hostname], here are your new
account details:

- username: [username]
- password: [password]
'''))


EmailsDirectory.register('new-account-approved',
        N_('Approval of new account'),
        N_('Available variables: username, password'),
        category = N_('Identification'),
        default_subject = N_('Your account has been approved'),
        default_body = N_('''\
Your account has been approved.

Account details:

- username: [username]
[if-any password]- password: [password][end]
'''))

EmailsDirectory.register('warning-about-unused-account',
        N_('Warning about unusued account'),
        N_('Available variables: username'),
        category = N_('Identification'),
        default_subject = N_('Your account is unused'),
        default_body = N_('''\
Your account ([username]) is not being used.
'''))

EmailsDirectory.register('notification-of-removed-account',
        N_('Notification of removal of unused account'),
        N_('Available variables: username'),
        category = N_('Identification'),
        default_subject = N_('Your account has been removed'),
        default_body = N_('''\
Your account ([username]) was not being used, it has therefore been removed.
'''))

EmailsDirectory.register('new-registration-admin-notification',
        N_('Notification of new registration to administrators'),
        N_('Available variables: hostname, email_as_username, username'),
        category = N_('Identification'),
        default_subject = N_('New Registration'),
        default_body = N_('''\
Hello,

A new account has been created on [hostname].

 - name: [name]
 - username: [username]
'''))

EmailsDirectory.register('new-account-generated-password',
        N_('Welcome email, with generated password'),
        N_('Available variables: hostname, username, password, email_as_username'),
        category = N_('Identification'),
        default_subject = N_('Welcome to [hostname]'),
        default_body = N_('''\
Welcome to [hostname],

Your password is: [password]
'''))

EmailsDirectory.register('password-email-create-anew',
        N_('Email with a new password for the user'),
        N_('Available variables: hostname, name, username, password'),
        category = N_('Identification'),
        default_subject = N_('Your new password for [hostname]'),
        default_body = N_('''\
Hello [name],

Here is your new password for [hostname]: [password]
'''))

EmailsDirectory.register('password-email-current',
        N_('Email with current password for the user'),
        N_('Available variables: hostname, name, username, password'),
        category = N_('Identification'),
        default_subject = N_('Your password for [hostname]'),
        default_body = N_('''\
Hello [name],

Here is your password for [hostname]: [password]
'''))


TextsDirectory.register('account-created-waiting-activation',
        N_('Text when account confirmed by user but waiting moderator approval'),
        category = N_('Identification'),
        default = N_('''<p>
Your account has been created.  In order to be effective
it must be activated by a moderator.  You will receive an
email when this is done.
</p>'''))

TextsDirectory.register('account-created',
        N_('Text when account confirmed by user'),
        category = N_('Identification'),
        default = N_('''<p>
Your account has been created.
</p>'''))

TextsDirectory.register('password-forgotten-token-sent',
        N_('Text when an email with a change password token has been sent'),
        category = N_('Identification'),
        default = N_('''<p>
A token for changing your password has been emailed to you. Follow the instructions in that email to change your password.
</p>
<p>
 <a href="login">Log In</a>
</p>'''))

TextsDirectory.register('new-password-sent-by-email',
        N_('Text when new password has been sent'),
        category = N_('Identification'),
        default = N_('''<p>
Your new password has been sent to you by email.
</p>
<p>
 <a href="login">Login</a>
</p>'''))

TextsDirectory.register('new-account', N_('Text on top of registration form'),
        category = N_('Identification'))

TextsDirectory.register('password-forgotten-link',
        N_('Text on login page, linking to the forgotten password request page'),
        category = N_('Identification'),
        default = N_('''<p>
If you have an account, but have forgotten your password, you should go
to the <a href="%(forgotten_url)s">Lost password page</a> and submit a request
to change your password.
</p>'''))

TextsDirectory.register('password-forgotten-enter-username',
        N_('Text on forgotten password request page'),
        category = N_('Identification'),
        default = N_('''<p>
If you have an account, but have forgotten your password, enter your user name
below and submit a request to change your password.
</p>'''))

TextsDirectory.register('password-account-link-to-register-page',
        N_('Text linking the login page to the account creation page'),
        hint = N_('Available variable: register_url'),
        category = N_('Identification'),
        default = N_('''<p>
If you do not have an account, you should go to the <a href="[register_url]">
New Account page</a>.
</p>'''))

TextsDirectory.register('invalid-password-token',
        N_('Text when an invalid password token is used'),
        category = N_('Identification'),
        default = N_('''<p>
Sorry, the token you used is invalid, or has already been used.
</p>'''))

TextsDirectory.register('top-of-login',
        N_('Text on top of the login page'),
        category = N_('Identification'))

TextsDirectory.register('email-sent-confirm-creation',
        N_('Text when a mail for confirmation of an account creation has been sent'),
        category = N_('Identification'),
        default = N_('An email has been sent to you so you can confirm your account creation.'))


def handle_unused_accounts(publisher):
    if not 'password' in get_cfg('identification', {}).get('methods', []):
        return
    identities_cfg = get_cfg('identities', {})
    warn_about_unused_account_delay = identities_cfg.get('warn_about_unused_account_delay', 0)
    remove_unused_account_delay = identities_cfg.get('remove_unused_account_delay', 0)
    if not (warn_about_unused_account_delay or remove_unused_account_delay):
        return
    for user in get_publisher().user_class.select():
        if getattr(user, 'is_admin', False):
            # do not apply the automatic removal of unused accounts for
            # administrators
            continue
        if not getattr(user, 'last_seen', None):
            continue
        if warn_about_unused_account_delay and (
                time.time() - user.last_seen) > 86400*warn_about_unused_account_delay:
            try:
                account = PasswordAccount.get_on_index(user.id, 'user_id')
            except KeyError:
                continue
            if not account.warned_about_unused_account:
                data = {
                    'username': account.id,
                }
                if user.email:
                    emails.custom_template_email('warning-about-unused-account', data, user.email)
                # XXX: notify admin too
                account.warned_about_unused_account = True
                account.store()

        if remove_unused_account_delay and (
                time.time() - user.last_seen) > 86400*remove_unused_account_delay:
            try:
                account = PasswordAccount.get_on_index(user.id, 'user_id')
            except KeyError:
                continue
            user.remove_self()
            account.remove_self()

            data = {
                'username': account.id,
            }
            if user.email:
                emails.custom_template_email('notification-of-removed-account', data, user.email)
            # XXX: notify admin too


def handle_expired_tokens(publisher):
    if not 'password' in get_cfg('identification', {}).get('methods', []):
        return
    now = time.time()
    for token_key in tokens.Token.keys():
        try:
            token = tokens.Token.get(token_key, ignore_migration=True)
        except KeyError:
            continue
        if token.type == 'account-confirmation' and now > token.expiration:
            try:
                account = PasswordAccount.get(token.username)
            except KeyError:
                # no such account, unncessary to keep the token
                token.remove_self()
                continue
            if not account.awaiting_confirmation:
                continue
            user = account.get_user()
            account.remove_self()
            # XXX: theorically the user could have been associated with another
            # account, it is ignored.
            if user:
                user.remove_self()
            token.remove_self()

if get_publisher_class():
    # at 6:00 in the morning, every day
    get_publisher_class().register_cronjob(
            CronJob(handle_unused_accounts, minutes=[0], hours=[6]))
    get_publisher_class().register_cronjob(
            CronJob(handle_expired_tokens, minutes=[10], hours=[6]))
