1698 lines
65 KiB
Python
1698 lines
65 KiB
Python
# 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=[0], hours=[6]))
|