Project

General

Profile

Download (23.6 KB) Statistics
| Branch: | Tag: | Revision:

root / extra / modules / ezldap_ui.ptl @ 8df4b83d

1
'''
2
Module to provide the kind of interaction needed for a eZ publish website
3
acting as reverse proxy and passing in an HTTP header a pointer to an LDAP
4
entry.
5
'''
6

    
7
# FIXME: try/except
8
import ldap
9
import ldap.modlist
10

    
11
import base64
12
import datetime
13
import random
14
import string
15
import urllib
16
from urlparse import urlparse
17
try:
18
    from hashlib import sha1 as sha
19
except ImportError:
20
    from sha import sha
21

    
22
from quixote import get_publisher, get_request, redirect, get_session, get_session_manager
23
from quixote.directory import Directory
24

    
25
from qommon import get_cfg, template, emails
26
from qommon import misc
27
from qommon.admin.emails import EmailsDirectory
28
from qommon.admin.texts import TextsDirectory
29
from qommon.form import *
30
from qommon.ident.password import check_password, make_password
31
from qommon import errors
32

    
33
from wcs.formdef import FormDef
34
from wcs.users import User
35

    
36
from myspace import MyspaceDirectory
37
from payments import is_payment_supported
38

    
39
import ezldap
40

    
41
def make_token():
42
    '''Create a token, checking it does not already exist in the ldap base.'''
43
    r = random.SystemRandom()
44
    ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
45
    ldap_conn = ezldap.get_ldap_conn()
46
    while True:
47
        token = ''.join([r.choice(string.letters + string.digits) for x in range(16)])
48
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
49
                filterstr='token=%s' % token)
50
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
51
        if result_data == []:
52
            break
53
    return token
54

    
55
def add_token(token, dn, action, exp_days=3):
56
    ''' Add a token in the LDAP  '''
57
    expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
58
    ldap_conn = ezldap.get_ldap_conn()
59
    # add token into the LDAP
60
    mod_list = [(ldap.MOD_REPLACE, 'token', token),
61
            (ldap.MOD_REPLACE, 'tokenAction', action),
62
            (ldap.MOD_REPLACE, 'tokenExpiration', expdate.strftime('%Y%m%d%H%M%SZ'))]
63
    ldap_conn.modify_s(dn, mod_list)
64
    return token
65

    
66
def email_to_admins(dn, template):
67
    admins = [x for x in get_publisher().user_class.select() if x.is_admin]
68
    if not admins:
69
        return
70
    admin_emails = [x.email for x in admins if x.email]
71

    
72
    user = ezldap.EzLdapUser(dn)
73
    data = {
74
        'hostname': get_request().get_server(),
75
        'username': user.email,
76
        'email_as_username': 'True',
77
        'name': user.display_name,
78
        'email': user.email,
79
    }
80
    get_publisher().substitutions.feed(user)
81

    
82
    emails.custom_ezt_email(template, data,
83
            admin_emails, fire_and_forget = True)
84

    
85

    
86
class EzMyspaceDirectory(MyspaceDirectory):
87

    
88
    def __init__(self):
89
        self._q_exports.extend(['change_email'])
90

    
91
    def _index_buttons [html] (self, form_data):
92
        super(EzMyspaceDirectory, self)._index_buttons(form_data)
93
        '<p class="command"><a href="change_email">%s</a></p>' % _('Change my email')
94
        '<br />'
95
        '<br />'
96
        '<p>'
97
        _('You can delete your account freely from the services portal. '
98
          'This action is irreversible; it will destruct your personal '
99
          'datas and destruct the access to your request history.')
100
        ' <strong><a href="remove">%s</a></strong>.' % _('Delete My Account')
101
        '</p>'
102

    
103
    def _my_profile [html] (self, user_formdef, user):
104
        '<h3 id="my-profile">%s</h3>' % _('My Profile')
105

    
106
        TextsDirectory.get_html_text('top-of-profile')
107

    
108
        if user.form_data:
109
            get_publisher().substitutions.feed(get_request().user)
110
            data = get_publisher().substitutions.get_context_variables()
111
            TextsDirectory.get_html_text('profile-presentation', data)
112
        else:
113
            '<p>%s</p>' % _('Empty profile')
114

    
115
    def submit_password(self, new_password):
116
        userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
117
        mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword)]
118
        ezldap.get_ldap_conn().modify_s(get_session().user, mod_list)
119

    
120
    def change_email [html] (self):
121
        form = Form(enctype = 'multipart/form-data')
122
        form.add(ezldap.EzEmailWidget, 'new_email', title = _('New email'),
123
                required=True, size=30)
124
        form.add_submit('submit', _('Submit Request'))
125
        form.add_submit('cancel', _('Cancel'))
126
        if form.get_submit() == 'cancel':
127
            return redirect('.')
128
        if form.is_submitted() and not form.has_errors():
129
            new_email = form.get_widget('new_email').parse()
130
            self.change_email_submit(new_email)
131
            template.html_top(_('Change email'))
132
            TextsDirectory.get_html_text('change-email-token-sent') % { 'new_email': new_email }
133
        else:
134
            template.html_top(_('Change email'))
135
            TextsDirectory.get_html_text('change-email')
136
            form.render()
137

    
138
    def change_email_submit(self, new_email):
139
        data = {}
140

    
141
        token = make_token()
142
        req = get_request()
143
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
144
                urllib.quote(req.environ.get('SCRIPT_NAME')))
145
        data['change_url'] = base_url + '/token/?token=%s' % token
146
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
147
        data['new_email'] = new_email
148
        data['current_email'] = req.user.email
149

    
150
        # add the token in the LDAP
151
        add_token(token, get_session().user, "change_email;%s" % new_email)
152
        # send mail
153
        emails.custom_ezt_email('change-email-request', data,
154
                data['new_email'], fire_and_forget = True)
155

    
156
    def remove [html] (self):
157
        user = get_request().user
158
        if not user or user.anonymous:
159
            raise errors.AccessUnauthorizedError()
160
        form = Form(enctype = 'multipart/form-data')
161
        form.add_submit('submit', _('Remove my account'))
162
        form.add_submit('cancel', _('Cancel'))
163
        if form.get_submit() == 'cancel':
164
            return redirect('.')
165
        if form.is_submitted() and not form.has_errors():
166
            self.remove_email_submit()
167
            template.html_top(_('Removing Account'))
168
            TextsDirectory.get_html_text('remove-token-sent') % { 'email': user.email }
169
        else:
170
            template.html_top(_('Removing Account'))
171
            TextsDirectory.get_html_text('remove-account')
172
            form.render()
173

    
174
    def remove_email_submit(self):
175
        data = {}
176
        token = make_token()
177
        req = get_request()
178
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
179
                urllib.quote(req.environ.get('SCRIPT_NAME')))
180
        data['remove_url'] = base_url + '/token/?token=%s' % token
181
        data['cancel_url'] = base_url + '/token/cancel?token=%s' % token
182
        data['email'] = req.user.email
183
        # add the token in the LDAP
184
        add_token(token, get_session().user, "remove")
185
        # send mail
186
        emails.custom_ezt_email('remove-request', data,
187
                data['email'], fire_and_forget = True)
188

    
189
class EzTokenDirectory(Directory):
190
    _q_exports = ['', 'cancel']
191

    
192
    def _q_index (self):
193
        '''token processing'''
194
        token = get_request().form.get('token')
195
        if token:
196
            result_data = self._get_ldap_entry_token(token)
197
            if result_data:
198
                return self._actions(result_data[0][1]['tokenAction'][0], result_data)
199
        template.html_top(_('Error'))
200
        return _('The token you submitted does not exist, has expired, or has been cancelled.')
201

    
202
    def _get_ldap_entry_token(self, token):
203
        ''' return a ldap result for the given token '''
204
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
205
        ldap_conn = ezldap.get_ldap_conn()
206
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
207
                    filterstr='token=%s' % token)
208
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
209
        if result_type == ldap.RES_SEARCH_ENTRY:
210
            return result_data
211
        else:
212
            return None
213

    
214
    def _actions(self, token_action, result_data):
215
        dn = result_data[0][0]
216
        action = token_action.split(";")[0]
217
        if action == 'create':
218
            mod_list = [(ldap.MOD_REPLACE, 'actif', 'TRUE'),
219
                    (ldap.MOD_DELETE, 'token', None),
220
                    (ldap.MOD_DELETE, 'tokenAction', None),
221
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
222
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
223
            email_to_admins(dn, 'new-registration-admin-notification')
224
            template.html_top(_('Welcome'))
225
            return TextsDirectory.get_html_text('account-created')
226
        elif action == 'change_email':
227
            new_email = token_action.split(";")[1]
228
            mod_list = [(ldap.MOD_REPLACE, ezldap.LDAP_EMAIL, str(new_email)),
229
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
230
                    (ldap.MOD_DELETE, 'token', None),
231
                    (ldap.MOD_DELETE, 'tokenAction', None),
232
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
233
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
234
            template.html_top(_('Email changed'))
235
            return TextsDirectory.get_html_text('new-email-confirmed') % { 'new_email': new_email }
236
        elif action == 'change_password':
237
            email = result_data[0][1][ezldap.LDAP_EMAIL][0]
238
            new_password = make_password()
239
            userPassword = ['{sha}%s' % base64.encodestring(sha(new_password).digest()).strip()]
240
            mod_list = [(ldap.MOD_REPLACE, 'userPassword', userPassword),
241
                    (ldap.MOD_REPLACE, 'actif', 'TRUE'),
242
                    (ldap.MOD_DELETE, 'token', None),
243
                    (ldap.MOD_DELETE, 'tokenAction', None),
244
                    (ldap.MOD_DELETE, 'tokenExpiration', None)]
245
            ezldap.get_ldap_conn().modify_s(dn, mod_list)
246
            data = {
247
                 'username': email,
248
                 'password': new_password,
249
                 'hostname': get_request().get_server(),
250
            }
251
            emails.custom_ezt_email('new-generated-password', data, email,
252
                    exclude_current_user = False)
253
            template.html_top(_('New password sent by email'))
254
            return TextsDirectory.get_html_text('new-password-sent-by-email')
255
        elif action == 'remove':
256
            template.html_top(_('Removing Account'))
257
            # email (just before remove)
258
            email_to_admins(dn, 'notification-of-removed-account')
259
            # delete all related forms
260
            formdefs = FormDef.select()
261
            user_forms = []
262
            for formdef in formdefs:
263
                user_forms.extend(formdef.data_class().get_with_indexed_value('user_id', dn))
264
            for formdata in user_forms:
265
                formdata.remove_self()
266
            # delete the user in ldap
267
            ezldap.get_ldap_conn().delete_s(dn)
268
            # delete session and redirect to login page
269
            get_session_manager().expire_session()
270
            root_url = get_publisher().get_root_url()
271
            return redirect('%slogin' % root_url)
272

    
273
    def cancel(self):
274
        token = get_request().form.get('token')
275
        if token:
276
            result_data = self._get_ldap_entry_token(token)
277
            if result_data:
278
                dn = result_data[0][0]
279
                mod_list = [(ldap.MOD_DELETE, 'token', None),
280
                        (ldap.MOD_DELETE, 'tokenAction', None),
281
                        (ldap.MOD_DELETE, 'tokenExpiration', None)]
282
                ezldap.get_ldap_conn().modify_s(dn, mod_list)
283
                template.html_top(_('Request cancelled'))
284
                return  _('Your request has been cancelled.')
285

    
286
        template.html_top(_('Error'))
287
        return _('The token you submitted does not exist or has expired')
288

    
289

    
290
class EzRegisterDirectory(Directory):
291
    _q_exports = ['', 'passwordreset']
292

    
293
    def _q_index(self):
294
        form = Form(enctype='multipart/form-data')
295

    
296
        template.html_top(_('Registration'))
297
        head = TextsDirectory.get_html_text('new-account')
298
        form.widgets.append(HtmlWidget(head))
299

    
300
        # build password hint : "at least 4 cars, at most 12"
301
        passwords_cfg = get_cfg('passwords', {})
302
        min_len = passwords_cfg.get('min_length', 0)
303
        max_len = passwords_cfg.get('max_length', 0)
304
        if min_len and max_len:
305
            password_hint = _('At least %(min)d characters, at most %(max)d') % \
306
                    {'min': min_len, 'max': max_len }
307
        elif min_len:
308
            password_hint = _('At least %d characters') % min_len
309
        elif max_len:
310
            password_hint = _('At most %d characters') % max_len
311
        else:
312
            password_hint = ''
313

    
314
        # get required attributes from User configuration
315
        for f in User.get_formdef().fields:
316
            if f.required:
317
                kwargs = {'required': True, 'value': form.get(f.varname)}
318
                for k in f.extra_attributes:
319
                    if hasattr(f, k):
320
                        kwargs[k] = getattr(f, k)
321
                f.perform_more_widget_changes(form, kwargs)
322
                form.add(f.widget_class, f.varname, title=f.label, hint=None, **kwargs)
323
        # static required fields
324
        form.add(ezldap.EzEmailWidget, ezldap.LDAP_EMAIL, title=_('Email'),
325
                value=form.get(ezldap.LDAP_EMAIL), required=True, size=30,
326
                hint=_('Remember: this will be your username'))
327
        form.add(PasswordWidget, '__password', title=_('New Password'),
328
                value=form.get('__password'), required=True, size=15,
329
                hint=password_hint)
330
        form.add(PasswordWidget, '__password2', title=_('New Password (confirm)'),
331
                value=form.get('__password2'), required=True, size=15)
332
        form.widgets.append(CaptchaWidget('__captcha'))
333
        form.add_submit('submit', _('Register'))
334
        form.add_submit('cancel', _('Cancel'))
335

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

    
339
        if form.is_submitted() and not form.has_errors():
340
            check_password(form, '__password')
341
            password = form.get_widget('__password').parse()
342
            password2 = form.get_widget('__password2').parse()
343
            if password != password2:
344
                form.set_error('__password2', _('Passwords do not match'))
345

    
346
        if form.is_submitted() and not form.has_errors():
347
            self.register_submit(form)
348
            template.html_top(_('Welcome'))
349
            return TextsDirectory.get_html_text('email-sent-confirm-creation')
350

    
351
        return form.render()
352

    
353
    def register_submit(self, form):
354
        data = {}
355
        exp_days = 3
356

    
357
        for widget in form.widgets:
358
            value = widget.parse()
359
            if value:
360
                if widget.name == '__password':
361
                    password = value
362
                    data['userPassword'] = ['{sha}%s' % \
363
                            base64.encodestring(sha(password).digest()).strip()]
364
                elif not widget.name.startswith('__'):
365
                    if widget.name.startswith('date'):
366
                        date = datetime.datetime.strptime(value, misc.date_format())
367
                        data[widget.name] = date.strftime('%Y%m%d%H%M%SZ')
368
                    else:
369
                        data[widget.name] = [value]
370

    
371
        misc_cfg = get_cfg('misc', {})
372
        expdate = datetime.datetime.utcnow() + datetime.timedelta(exp_days)
373
        data['actif'] = 'FALSE'
374
        data['token'] = make_token()
375
        data['tokenAction'] = 'create'
376
        data['tokenExpiration'] = expdate.strftime('%Y%m%d%H%M%SZ')
377
        req = get_request()
378
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
379
                urllib.quote(req.environ.get('SCRIPT_NAME')))
380
        token_url = base_url + '/token/?token=%s' % data['token']
381

    
382
        data['objectClass'] = 'vincennesCitoyen'
383
        mod_list = ldap.modlist.addModlist(data)
384
        ldap_dntemplate = misc_cfg.get('aq-ezldap-dntemplate')
385
        while True:
386
            dn = ldap_dntemplate % random.randint(100000, 5000000)
387
            try:
388
                ezldap.get_ldap_conn().add_s(dn, mod_list)
389
            except ldap.ALREADY_EXISTS:
390
                continue
391
            break
392

    
393
        data = {
394
            'email': data[ezldap.LDAP_EMAIL][0],
395
            'username': data[ezldap.LDAP_EMAIL][0],
396
            'password': password,
397
            'token_url': token_url,
398
            'website': base_url,
399
            'admin_email': '',
400
        }
401
        emails.custom_ezt_email('password-subscription-notification', data,
402
                    data.get('email'), fire_and_forget = True)
403

    
404

    
405
    def passwordreset [html] (self):
406
        form = Form(enctype='multipart/form-data')
407
        form.add(EmailWidget, 'email', title=_('Email'), required=True, size=30)
408
        form.add_submit('change', _('Submit Request'))
409

    
410
        if form.is_submitted() and not form.has_errors():
411
            email = form.get_widget('email').parse()
412
            dn = self.get_dn_by_email(email)
413
            if dn:
414
                self.passwordreset_submit(email, dn)
415
                template.html_top(_('Forgotten Password'))
416
                TextsDirectory.get_html_text(str('password-forgotten-token-sent'))
417
            else:
418
                form.set_error('email', _('There is no user with that email.'))
419
                template.html_top(_('Forgotten Password'))
420
                TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
421
                form.render()
422
        else:
423
            template.html_top(_('Forgotten Password'))
424
            TextsDirectory.get_html_text(str('password-forgotten-enter-username'))
425
            form.render()
426

    
427
    def get_dn_by_email(self, email):
428
        ldap_conn = ezldap.get_ldap_conn()
429
        ldap_basedn = get_cfg('misc', {}).get('aq-ezldap-basedn')
430
        ldap_result_id = ldap_conn.search(ldap_basedn, ldap.SCOPE_SUBTREE,
431
                    filterstr='%s=%s' % (ezldap.LDAP_EMAIL, email))
432
        result_type, result_data = ldap_conn.result(ldap_result_id, all=0)
433
        if result_type == ldap.RES_SEARCH_ENTRY:
434
            return result_data[0][0] # dn
435
        else:
436
            return None
437

    
438
    def passwordreset_submit(self, email, dn):
439
        token = make_token()
440
        # add token into the LDAP
441
        add_token(token, dn, "change_password")
442
        # send mail
443
        req = get_request()
444
        base_url = '%s://%s%s' % (req.get_scheme(), req.get_server(),
445
                urllib.quote(req.environ.get('SCRIPT_NAME')))
446
        data = { 'change_url': base_url + '/token/?token=%s' % token,
447
                'cancel_url': base_url + '/token/cancel?token=%s' % token,
448
                # FIXME: set the expiration date here
449
                'time': None }
450
        emails.custom_ezt_email('change-password-request', data,
451
                email, exclude_current_user = False)
452

    
453

    
454
def restore_legacy_root(root_directory):
455
    root_directory.myspace = MyspaceDirectory()
456
    from root import AlternateRegisterDirectory
457
    root_directory.register = AlternateRegisterDirectory()
458

    
459
def try_auth(root_directory):
460
    misc_cfg = get_cfg('misc', {})
461
    ldap_url = misc_cfg.get('aq-ezldap-url')
462

    
463
    if not ldap_url:
464
        # no ldap: restore non monkey patched classes & dir
465
        get_publisher().user_class = User
466
        restore_legacy_root(root_directory)
467
        return
468

    
469
    # activate the "magic user" (legacy+ldap)
470
    get_publisher().user_class = ezldap.EzMagicUser
471

    
472
    # reverse proxy detection
473
    reverse_url = get_request().get_header('X-WCS-ReverseProxy-URL')
474
    if reverse_url:
475
        # gorilla patching : SCRIPT_NAME/get_server/get_scheme
476
        parsed = urlparse(reverse_url)
477
        if parsed.path[-1:] == '/':
478
            get_request().environ['SCRIPT_NAME'] = parsed.path[:-1]
479
        else:
480
            get_request().environ['SCRIPT_NAME'] = parsed.path
481
        get_request().get_server = lambda clean=True: parsed.netloc
482
        get_request().get_scheme = lambda : parsed.scheme
483
    else:
484
        restore_legacy_root(root_directory)
485
        return
486

    
487
    # the request must come from the reverse-proxy IP
488
    ezldap_ip = misc_cfg.get('aq-ezldap-ip', None)
489
    if ezldap_ip and get_request().environ.get('REMOTE_ADDR') != ezldap_ip:
490
        # not a good IP: restore non monkey patched classes & dir
491
        restore_legacy_root(root_directory)
492
        return
493

    
494
    # add ldap register & token (anonymous actions)
495
    root_directory.register = EzRegisterDirectory()
496
    root_directory.token = EzTokenDirectory()
497

    
498
    # does the reverse-proxy provide the user DN ?
499
    user_dn = get_request().get_header('X-Auquotidien-Auth-Dn')
500
    session = get_session()
501
    if user_dn:
502
        # this is a LDAP user: set session, activate special myspace
503
        session.set_user(user_dn)
504
        root_directory.myspace = EzMyspaceDirectory()
505
    else:
506
        # disconnected
507
        session.set_user(None)
508
        root_directory.myspace = MyspaceDirectory()
509

    
510

    
511
EmailsDirectory.register('aq-change-email-request',
512
        N_('Request for email change'),
513
        N_('Available variables: new_email, current_email, change_url, cancel_url'),
514
        category = N_('Identification'),
515
        default_subject = N_('Change email Request'),
516
        default_body = N_('''
517
You have requested to associate your account with the email address : [new_email]
518

    
519
Warning : this email will become your username.
520

    
521
To complete the change, visit the following link:
522
[change_url]
523

    
524
If you are not the person who made this request, or you wish to cancel
525
this request, visit the following link:
526
[cancel_url]
527

    
528
If you cancel the contact email change request, your account will remain with
529
your current email ([current_email]).
530

    
531
If you do nothing, the request will lapse after 3 days.
532
'''))
533

    
534
TextsDirectory.register('aq-change-email',
535
        N_('Text when user want to change his email'),
536
        category = N_('Identification'),
537
        default = N_('''
538
You can change your email address here.
539
'''))
540

    
541
TextsDirectory.register('aq-change-email-token-sent',
542
        N_('Text after user had requested to change his email'),
543
        category = N_('Identification'),
544
        default = N_('''
545
A confirmation email has been sent to your new address (%(new_email)s).
546
Follow the instructions in that email to validate your request.
547
'''))
548

    
549
TextsDirectory.register('aq-new-email-confirmed',
550
        N_('Text when new email confirmed by user'),
551
        category = N_('Identification'),
552
        default = N_('''
553
Your email has been changed to : %(new_email)s.
554
Remember that it's your new username !
555
'''))
556

    
557
TextsDirectory.register('aq-profile-presentation',
558
        N_('Profile presentation'),
559
        hint = N_('variables: user fields varnames'),
560
        default = N_('''<p>
561
<ul>
562
  <li>Email: [user_email]</li>
563
</ul>
564
</p>'''))
565

    
566
EmailsDirectory.register('aq-remove-request',
567
        N_('Delete account request'),
568
        N_('Available variables: email, remove_url, cancel_url'),
569
        category = N_('Identification'),
570
        default_subject = N_('Delete account request'),
571
        default_body = N_('''
572
You have requested to *delete* your account [email]
573

    
574
Warning: this action is irreversible; it will destruct your personal
575
datas and destruct the access to your request history.
576

    
577
To complete the request, visit the following link:
578
[remove_url]
579

    
580
If you are not the person who made this request, or you wish to cancel this
581
request, visit the following link:
582
[cancel_url]
583

    
584
If you do nothing, the request will lapse after 3 days.
585
'''))
586

    
587
TextsDirectory.register('aq-remove-account',
588
        N_('Text when user want to delete his account'),
589
        category = N_('Identification'),
590
        default = N_('Are you really sure you want to remove your account?'))
591

    
592
TextsDirectory.register('aq-remove-token-sent',
593
        N_('Text after user had requested to delete his account'),
594
        category = N_('Identification'),
595
        default = N_('''
596
A confirmation email has been sent to your email address.
597
Follow the instructions in that email to validate your request.
598
'''))
599

    
(13-13/26)