1 | # -*- coding: utf-8 -*- |
---|
2 | # |
---|
3 | # Copyright (C) 2005 Matthew Good <trac@matt-good.net> |
---|
4 | # Copyright (C) 2010-2013 Steffen Hoffmann <hoff.st@web.de> |
---|
5 | # All rights reserved. |
---|
6 | # |
---|
7 | # This software is licensed as described in the file COPYING, which |
---|
8 | # you should have received as part of this distribution. |
---|
9 | # |
---|
10 | # Author: Matthew Good <trac@matt-good.net> |
---|
11 | |
---|
12 | import base64 |
---|
13 | import re |
---|
14 | import time |
---|
15 | |
---|
16 | from genshi.builder import tag |
---|
17 | from genshi.core import Markup |
---|
18 | from os import urandom |
---|
19 | |
---|
20 | from trac import perm, util |
---|
21 | from trac.core import Component, TracError, implements |
---|
22 | from trac.config import BoolOption, Option |
---|
23 | from trac.env import open_environment |
---|
24 | from trac.web import auth, chrome |
---|
25 | from trac.web.main import IRequestHandler, IRequestFilter |
---|
26 | |
---|
27 | from acct_mgr.api import AccountManager, CommonTemplateProvider |
---|
28 | from acct_mgr.api import IAccountRegistrationInspector |
---|
29 | from acct_mgr.api import _, N_, dgettext, gettext, tag_ |
---|
30 | from acct_mgr.model import email_associated, get_user_attribute |
---|
31 | from acct_mgr.model import set_user_attribute |
---|
32 | from acct_mgr.util import containsAny, is_enabled |
---|
33 | |
---|
34 | |
---|
35 | class RegistrationError(TracError): |
---|
36 | """Exception raised when a registration check fails.""" |
---|
37 | |
---|
38 | def __init__(self, message, *args, **kwargs): |
---|
39 | """TracError sub-class with extended i18n support. |
---|
40 | |
---|
41 | It eases error initialization with messages optionally including |
---|
42 | arguments meant for string substitution after deferred translation. |
---|
43 | """ |
---|
44 | title = N_("Registration Error") |
---|
45 | tb = 'show_traceback' |
---|
46 | # Care for the 2nd TracError standard keyword argument only. |
---|
47 | show_traceback = tb in kwargs and kwargs.pop(tb, False) |
---|
48 | super(RegistrationError, self).__init__(message, title, show_traceback) |
---|
49 | self.msg_args = args |
---|
50 | |
---|
51 | |
---|
52 | class GenericRegistrationInspector(Component): |
---|
53 | """Generic check class, great for creating simple checks quickly.""" |
---|
54 | |
---|
55 | implements(IAccountRegistrationInspector) |
---|
56 | |
---|
57 | abstract = True |
---|
58 | |
---|
59 | @property |
---|
60 | def doc(self): |
---|
61 | return N_(self.__class__.__doc__) |
---|
62 | |
---|
63 | def render_registration_fields(self, req, data): |
---|
64 | """Emit one or multiple additional fields for registration form built. |
---|
65 | |
---|
66 | Returns a dict containing a 'required' and/or 'optional' tuple of |
---|
67 | * Genshi Fragment or valid XHTML markup for registration form |
---|
68 | * modified or unchanged data object (used to render `register.html`) |
---|
69 | If the return value is just a single tuple, its fragment or markup |
---|
70 | will be inserted into the 'required' section. |
---|
71 | """ |
---|
72 | template = '' |
---|
73 | return template, data |
---|
74 | |
---|
75 | def validate_registration(self, req): |
---|
76 | """Check registration form input. |
---|
77 | |
---|
78 | Returns a RegistrationError with error message, or None on success. |
---|
79 | """ |
---|
80 | # Nicer than a plain NotImplementedError. |
---|
81 | raise NotImplementedError, _( |
---|
82 | "No check method 'validate_registration' defined in %(module)s", |
---|
83 | module=self.__class__.__name__) |
---|
84 | |
---|
85 | |
---|
86 | class BasicCheck(GenericRegistrationInspector): |
---|
87 | """A collection of basic checks. |
---|
88 | |
---|
89 | This includes checking for |
---|
90 | * emptiness (no user input for username and/or password) |
---|
91 | * some blacklisted username characters |
---|
92 | * upper-cased usernames (reserved for Trac permission actions) |
---|
93 | * some reserved usernames |
---|
94 | * a username duplicate in configured password stores |
---|
95 | |
---|
96 | ''This check is bypassed for requests regarding user's own preferences.'' |
---|
97 | """ |
---|
98 | |
---|
99 | def validate_registration(self, req): |
---|
100 | if req.path_info == '/prefs': |
---|
101 | return |
---|
102 | |
---|
103 | acctmgr = AccountManager(self.env) |
---|
104 | username = acctmgr.handle_username_casing( |
---|
105 | req.args.get('username', '').strip()) |
---|
106 | |
---|
107 | if not username: |
---|
108 | raise RegistrationError(N_("Username cannot be empty.")) |
---|
109 | |
---|
110 | # Always exclude some special characters, i.e. |
---|
111 | # ':' can't be used in HtPasswdStore |
---|
112 | # '[' and ']' can't be used in SvnServePasswordStore |
---|
113 | blacklist = acctmgr.username_char_blacklist |
---|
114 | if containsAny(username, blacklist): |
---|
115 | pretty_blacklist = '' |
---|
116 | for c in blacklist: |
---|
117 | if pretty_blacklist == '': |
---|
118 | pretty_blacklist = tag(' \'', tag.b(c), '\'') |
---|
119 | else: |
---|
120 | pretty_blacklist = tag(pretty_blacklist, |
---|
121 | ', \'', tag.b(c), '\'') |
---|
122 | raise RegistrationError(N_( |
---|
123 | "The username must not contain any of these characters: %s"), |
---|
124 | tag.b(pretty_blacklist) |
---|
125 | ) |
---|
126 | |
---|
127 | # All upper-cased names are reserved for permission action names. |
---|
128 | if username.isupper(): |
---|
129 | raise RegistrationError(N_( |
---|
130 | "A username with only upper-cased characters is not allowed.") |
---|
131 | ) |
---|
132 | |
---|
133 | # Prohibit some user names, that are important for Trac and therefor |
---|
134 | # reserved, even if not in the permission store for some reason. |
---|
135 | if username.lower() in ['anonymous', 'authenticated']: |
---|
136 | raise RegistrationError(N_("Username %s is not allowed."), |
---|
137 | tag.b(username) |
---|
138 | ) |
---|
139 | |
---|
140 | # NOTE: A user may exist in a password store but not in the permission |
---|
141 | # store. I.e. this happens, when the user (from the password store) |
---|
142 | # never logged in into Trac. So we have to perform this test here |
---|
143 | # and cannot just check for the user being in the permission store. |
---|
144 | # And better obfuscate whether an existing user or group name |
---|
145 | # was responsible for rejection of this user name. |
---|
146 | for store_user in acctmgr.get_users(): |
---|
147 | # Do it carefully by disregarding case. |
---|
148 | if store_user.lower() == username.lower(): |
---|
149 | raise RegistrationError(N_( |
---|
150 | "Another account or group already exists, who's name " |
---|
151 | "differs from %s only by case or is identical."), |
---|
152 | tag.b(username) |
---|
153 | ) |
---|
154 | |
---|
155 | # Password consistency checks follow. |
---|
156 | password = req.args.get('password') |
---|
157 | if not password: |
---|
158 | raise RegistrationError(N_("Password cannot be empty.")) |
---|
159 | elif password != req.args.get('password_confirm'): |
---|
160 | raise RegistrationError(N_("The passwords must match.")) |
---|
161 | |
---|
162 | |
---|
163 | class BotTrapCheck(GenericRegistrationInspector): |
---|
164 | """A collection of simple bot checks. |
---|
165 | |
---|
166 | ''This check is bypassed for requests by an authenticated user.'' |
---|
167 | """ |
---|
168 | |
---|
169 | reg_basic_token = Option('account-manager', 'register_basic_token', '', |
---|
170 | doc="A string required as input to pass verification.") |
---|
171 | |
---|
172 | def render_registration_fields(self, req, data): |
---|
173 | """Add a hidden text input field to the registration form, and |
---|
174 | a visible one with mandatory input as well, if token is configured. |
---|
175 | """ |
---|
176 | if self.reg_basic_token: |
---|
177 | # Preserve last input for editing on failure instead of typing |
---|
178 | # everything again. |
---|
179 | old_value = req.args.get('basic_token', '') |
---|
180 | |
---|
181 | # TRANSLATOR: Hint for visible bot trap registration input field. |
---|
182 | hint = tag.p(Markup(_( |
---|
183 | """Please type [%(token)s] as verification token, |
---|
184 | exactly replicating everything within the braces.""", |
---|
185 | token=tag.b(self.reg_basic_token))), class_='hint') |
---|
186 | insert = tag( |
---|
187 | tag.label(_("Parole:"), |
---|
188 | tag.input(type='text', name='basic_token', size=20, |
---|
189 | class_='textwidget', value=old_value)), |
---|
190 | hint |
---|
191 | ) |
---|
192 | else: |
---|
193 | insert = None |
---|
194 | # TRANSLATOR: Registration form hint for hidden bot trap input field. |
---|
195 | insert = tag(insert, |
---|
196 | tag.input(type='hidden', name='sentinel', |
---|
197 | title=_("Better do not fill this field.")) |
---|
198 | ) |
---|
199 | return insert, data |
---|
200 | |
---|
201 | def validate_registration(self, req): |
---|
202 | if req.authname and req.authname != 'anonymous': |
---|
203 | return |
---|
204 | # Input must be an exact replication of the required token. |
---|
205 | basic_token = req.args.get('basic_token', '') |
---|
206 | # Unlike the former, the hidden bot-trap input field must stay empty. |
---|
207 | keep_empty = req.args.get('sentinel', '') |
---|
208 | if keep_empty or self.reg_basic_token and \ |
---|
209 | self.reg_basic_token != basic_token: |
---|
210 | raise RegistrationError(N_("Are you human? If so, try harder!")) |
---|
211 | |
---|
212 | |
---|
213 | class EmailCheck(GenericRegistrationInspector): |
---|
214 | """A collection of checks for email addresses. |
---|
215 | |
---|
216 | ''This check is bypassed, if account verification is disabled.'' |
---|
217 | """ |
---|
218 | |
---|
219 | def render_registration_fields(self, req, data): |
---|
220 | """Add an email address text input field to the registration form.""" |
---|
221 | # Preserve last input for editing on failure instead of typing |
---|
222 | # everything again. |
---|
223 | old_value = req.args.get('email', '').strip() |
---|
224 | insert = tag.label(_("Email:"), |
---|
225 | tag.input(type='text', name='email', size=20, |
---|
226 | class_='textwidget', value=old_value) |
---|
227 | ) |
---|
228 | # Deferred import required to aviod circular import dependencies. |
---|
229 | from acct_mgr.web_ui import AccountModule |
---|
230 | reset_password = AccountModule(self.env).reset_password_enabled |
---|
231 | verify_account = is_enabled(self.env, EmailVerificationModule) and \ |
---|
232 | EmailVerificationModule(self.env).verify_email |
---|
233 | if verify_account: |
---|
234 | # TRANSLATOR: Registration form hints for a mandatory input field. |
---|
235 | hint = tag.p(_("""The email address is required for Trac to send |
---|
236 | you a verification token."""), class_='hint') |
---|
237 | if reset_password: |
---|
238 | hint = tag(hint, tag.p(_( |
---|
239 | """Entering your email address will also enable you |
---|
240 | to reset your password if you ever forget it."""), |
---|
241 | class_='hint') |
---|
242 | ) |
---|
243 | return tag(insert, hint), data |
---|
244 | elif reset_password: |
---|
245 | # TRANSLATOR: Registration form hint, if email input is optional. |
---|
246 | hint = tag.p(_("""Entering your email address will enable you to |
---|
247 | reset your password if you ever forget it."""), |
---|
248 | class_='hint') |
---|
249 | return dict(optional=tag(insert, hint)), data |
---|
250 | else: |
---|
251 | # Always return the email text input itself as optional field. |
---|
252 | return dict(optional=insert), data |
---|
253 | |
---|
254 | def validate_registration(self, req): |
---|
255 | acctmgr = AccountManager(self.env) |
---|
256 | email = req.args.get('email', '').strip() |
---|
257 | if is_enabled(self.env, EmailVerificationModule) and \ |
---|
258 | EmailVerificationModule(self.env).verify_email: |
---|
259 | # Initial configuration case. |
---|
260 | if not email and not req.args.get('active'): |
---|
261 | raise RegistrationError(N_( |
---|
262 | "You must specify a valid email address.") |
---|
263 | ) |
---|
264 | # User preferences case. |
---|
265 | elif req.path_info == '/prefs' and email == req.session.get('email'): |
---|
266 | return |
---|
267 | elif email_associated(self.env, email): |
---|
268 | raise RegistrationError(N_( |
---|
269 | "The email address specified is already in use. " |
---|
270 | "Please specify a different one.") |
---|
271 | ) |
---|
272 | |
---|
273 | |
---|
274 | class RegExpCheck(GenericRegistrationInspector): |
---|
275 | """A collection of checks based on regular expressions. |
---|
276 | |
---|
277 | ''It depends on !EmailCheck being enabled too for using it's input field. |
---|
278 | Likewise email checking is bypassed, if account verification is disabled.'' |
---|
279 | """ |
---|
280 | |
---|
281 | username_regexp = Option('account-manager', 'username_regexp', |
---|
282 | r'(?i)^[A-Z0-9.\-_]{5,}$', |
---|
283 | doc="A validation regular expression describing new usernames.") |
---|
284 | email_regexp = Option('account-manager', 'email_regexp', |
---|
285 | r'(?i)^[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', |
---|
286 | doc="A validation regular expression describing new account emails.") |
---|
287 | |
---|
288 | def validate_registration(self, req): |
---|
289 | acctmgr = AccountManager(self.env) |
---|
290 | |
---|
291 | username = acctmgr.handle_username_casing( |
---|
292 | req.args.get('username', '').strip()) |
---|
293 | if req.path_info != '/prefs' and self.username_regexp != "" and \ |
---|
294 | not re.match(self.username_regexp.strip(), username): |
---|
295 | raise RegistrationError(N_( |
---|
296 | "Username %s doesn't match local naming policy."), |
---|
297 | tag.b(username) |
---|
298 | ) |
---|
299 | |
---|
300 | email = req.args.get('email', '').strip() |
---|
301 | if is_enabled(self.env, EmailCheck) and \ |
---|
302 | is_enabled(self.env, EmailVerificationModule) and \ |
---|
303 | EmailVerificationModule(self.env).verify_email: |
---|
304 | if self.email_regexp.strip() != "" and \ |
---|
305 | not re.match(self.email_regexp.strip(), email) and \ |
---|
306 | not req.args.get('active'): |
---|
307 | raise RegistrationError(N_( |
---|
308 | "The email address specified appears to be invalid. " |
---|
309 | "Please specify a valid email address.") |
---|
310 | ) |
---|
311 | |
---|
312 | |
---|
313 | class UsernamePermCheck(GenericRegistrationInspector): |
---|
314 | """Check for usernames referenced in the permission system. |
---|
315 | |
---|
316 | ''This check is bypassed for requests by an authenticated user.'' |
---|
317 | """ |
---|
318 | |
---|
319 | def validate_registration(self, req): |
---|
320 | if req.authname and req.authname != 'anonymous': |
---|
321 | return |
---|
322 | username = AccountManager(self.env).handle_username_casing( |
---|
323 | req.args.get('username', '').strip()) |
---|
324 | |
---|
325 | # NOTE: We can't use 'get_user_permissions(username)' here |
---|
326 | # as this always returns a list - even if the user doesn't exist. |
---|
327 | # In this case the permissions of "anonymous" are returned. |
---|
328 | # |
---|
329 | # Also note that we can't simply compare the result of |
---|
330 | # 'get_user_permissions(username)' to some known set of permission, |
---|
331 | # i.e. "get_user_permissions('authenticated') as this is always |
---|
332 | # false when 'username' is the name of an existing permission group. |
---|
333 | # |
---|
334 | # And again obfuscate whether an existing user or group name |
---|
335 | # was responsible for rejection of this username. |
---|
336 | for (perm_user, perm_action) in \ |
---|
337 | perm.PermissionSystem(self.env).get_all_permissions(): |
---|
338 | if perm_user.lower() == username.lower(): |
---|
339 | raise RegistrationError(N_( |
---|
340 | "Another account or group already exists, who's name " |
---|
341 | "differs from %s only by case or is identical."), |
---|
342 | tag.b(username) |
---|
343 | ) |
---|
344 | |
---|
345 | |
---|
346 | class RegistrationModule(CommonTemplateProvider): |
---|
347 | """Provides users the ability to register a new account. |
---|
348 | |
---|
349 | Requires configuration of the AccountManager module in trac.ini. |
---|
350 | """ |
---|
351 | |
---|
352 | implements(chrome.INavigationContributor, IRequestHandler) |
---|
353 | |
---|
354 | require_approval = BoolOption( |
---|
355 | 'account-manager', 'require_approval', False, |
---|
356 | doc="Whether account registration requires administrative approval " |
---|
357 | "to enable the account or not.") |
---|
358 | |
---|
359 | def __init__(self): |
---|
360 | self.acctmgr = AccountManager(self.env) |
---|
361 | self._enable_check(log=True) |
---|
362 | |
---|
363 | def _enable_check(self, log=False): |
---|
364 | env = self.env |
---|
365 | writable = self.acctmgr.supports('set_password') |
---|
366 | ignore_case = auth.LoginModule(env).ignore_case |
---|
367 | if log: |
---|
368 | if not writable: |
---|
369 | self.log.warn('RegistrationModule is disabled because the ' |
---|
370 | 'password store does not support writing.') |
---|
371 | if ignore_case: |
---|
372 | self.log.debug('RegistrationModule will allow lowercase ' |
---|
373 | 'usernames only and convert them forcefully ' |
---|
374 | 'as required, while \'ignore_auth_case\' is ' |
---|
375 | 'enabled in [trac] section of your trac.ini.') |
---|
376 | return is_enabled(env, self.__class__) and writable |
---|
377 | |
---|
378 | enabled = property(_enable_check) |
---|
379 | |
---|
380 | # INavigationContributor methods |
---|
381 | |
---|
382 | def get_active_navigation_item(self, req): |
---|
383 | return 'register' |
---|
384 | |
---|
385 | def get_navigation_items(self, req): |
---|
386 | if not self.enabled: |
---|
387 | return |
---|
388 | if req.authname == 'anonymous': |
---|
389 | yield 'metanav', 'register', tag.a(_("Register"), |
---|
390 | href=req.href.register()) |
---|
391 | |
---|
392 | # IRequestHandler methods |
---|
393 | |
---|
394 | def match_request(self, req): |
---|
395 | return req.path_info == '/register' and self._enable_check(log=True) |
---|
396 | |
---|
397 | def process_request(self, req): |
---|
398 | acctmgr = self.acctmgr |
---|
399 | if req.authname != 'anonymous': |
---|
400 | req.redirect(req.href.prefs('account')) |
---|
401 | action = req.args.get('action') |
---|
402 | name = req.args.get('name', '').strip() |
---|
403 | username = acctmgr.handle_username_casing(req.args.get('username', |
---|
404 | '').strip()) |
---|
405 | data = { |
---|
406 | '_dgettext': dgettext, |
---|
407 | 'acctmgr': dict(name=name, username=username), |
---|
408 | 'ignore_auth_case': self.config.getbool('trac', 'ignore_auth_case') |
---|
409 | } |
---|
410 | verify_enabled = is_enabled(self.env, EmailVerificationModule) and \ |
---|
411 | EmailVerificationModule(self.env).verify_email |
---|
412 | data['verify_account_enabled'] = verify_enabled |
---|
413 | if req.method == 'POST' and action == 'create': |
---|
414 | try: |
---|
415 | # Check request and prime account on success. |
---|
416 | acctmgr.validate_account(req, True) |
---|
417 | except RegistrationError, e: |
---|
418 | # Attempt deferred translation. |
---|
419 | message = gettext(e.message) |
---|
420 | # Check for (matching number of) message arguments before |
---|
421 | # attempting string substitution. |
---|
422 | if e.msg_args and \ |
---|
423 | len(e.msg_args) == len(re.findall('%s', message)): |
---|
424 | message = message % e.msg_args |
---|
425 | chrome.add_warning(req, Markup(message)) |
---|
426 | else: |
---|
427 | if self.require_approval: |
---|
428 | set_user_attribute(self.env, username, 'approval', |
---|
429 | N_('pending')) |
---|
430 | # Notify admin user about registration pending for review. |
---|
431 | acctmgr._notify('registration_approval_required', |
---|
432 | username) |
---|
433 | chrome.add_notice(req, Markup(tag.span(Markup(_( |
---|
434 | "Your username has been registered successfully, but " |
---|
435 | "your account requires administrative approval. " |
---|
436 | "Please proceed according to local policy.")))) |
---|
437 | ) |
---|
438 | if verify_enabled: |
---|
439 | chrome.add_notice(req, Markup(tag.span(Markup(_( |
---|
440 | """Your username has been successfully registered but |
---|
441 | your account still requires activation. Please login |
---|
442 | as user %(user)s, and follow the instructions.""", |
---|
443 | user=tag.b(username))))) |
---|
444 | ) |
---|
445 | req.redirect(req.href.login()) |
---|
446 | chrome.add_notice(req, Markup(tag.span(Markup(_( |
---|
447 | """Registration has been finished successfully. |
---|
448 | You may log in as user %(user)s now.""", |
---|
449 | user=tag.b(username))))) |
---|
450 | ) |
---|
451 | req.redirect(req.href.login()) |
---|
452 | # Collect additional fields from IAccountRegistrationInspector's. |
---|
453 | fragments = dict(required=[], optional=[]) |
---|
454 | for inspector in acctmgr.register_checks: |
---|
455 | try: |
---|
456 | fragment, f_data = inspector.render_registration_fields(req, |
---|
457 | data) |
---|
458 | except TypeError, e: |
---|
459 | # Add some robustness by logging the most likely errors. |
---|
460 | self.env.log.warn("%s.render_registration_fields failed: %s" |
---|
461 | % (inspector.__class__.__name__, e)) |
---|
462 | fragment = None |
---|
463 | if fragment: |
---|
464 | try: |
---|
465 | # Python<2.5: Can't have 'except' and 'finally' in same |
---|
466 | # 'try' statement together. |
---|
467 | try: |
---|
468 | if 'optional' in fragment.keys(): |
---|
469 | fragments['optional'].append(fragment['optional']) |
---|
470 | except AttributeError: |
---|
471 | # No dict, just append Genshi Fragment or str/unicode. |
---|
472 | fragments['required'].append(fragment) |
---|
473 | else: |
---|
474 | fragments['required'].append(fragment.get('required', |
---|
475 | '')) |
---|
476 | finally: |
---|
477 | data.update(f_data) |
---|
478 | data['required_fields'] = fragments['required'] |
---|
479 | data['optional_fields'] = fragments['optional'] |
---|
480 | return 'register.html', data, None |
---|
481 | |
---|
482 | |
---|
483 | class EmailVerificationModule(CommonTemplateProvider): |
---|
484 | """Performs email verification on every new or changed address. |
---|
485 | |
---|
486 | A working email sender for Trac (!TracNotification or !TracAnnouncer) |
---|
487 | is strictly required to enable this module's functionality. |
---|
488 | |
---|
489 | Anonymous users should register and perms should be tweaked, so that |
---|
490 | anonymous users can't edit wiki pages and change or create tickets. |
---|
491 | So this email verification code won't be used on them. |
---|
492 | """ |
---|
493 | |
---|
494 | implements(IRequestFilter, IRequestHandler) |
---|
495 | |
---|
496 | verify_email = BoolOption( |
---|
497 | 'account-manager', 'verify_email', True, |
---|
498 | doc="Verify the email address of Trac users.") |
---|
499 | |
---|
500 | def __init__(self, *args, **kwargs): |
---|
501 | self.email_enabled = True |
---|
502 | if self.config.getbool('announcer', 'email_enabled') != True and \ |
---|
503 | self.config.getbool('notification', 'smtp_enabled') != True: |
---|
504 | self.email_enabled = False |
---|
505 | if is_enabled(self.env, self.__class__) == True: |
---|
506 | self.env.log.warn( |
---|
507 | ' '.join([self.__class__.__name__, |
---|
508 | "can't work because of missing email setup."]) |
---|
509 | ) |
---|
510 | |
---|
511 | # IRequestFilter methods |
---|
512 | |
---|
513 | def pre_process_request(self, req, handler): |
---|
514 | if not req.authname or req.authname == 'anonymous': |
---|
515 | # Permissions for anonymous users remain unchanged. |
---|
516 | return handler |
---|
517 | elif req.path_info == '/prefs' and req.method == 'POST' and \ |
---|
518 | not 'restore' in req.args: |
---|
519 | try: |
---|
520 | AccountManager(self.env).validate_account(req) |
---|
521 | # Check passed without error: New email address seems good. |
---|
522 | except RegistrationError, e: |
---|
523 | # Always warn about issues. |
---|
524 | chrome.add_warning( |
---|
525 | req, Markup(gettext(e.message))) |
---|
526 | # Look, if the issue existed before. |
---|
527 | attributes = get_user_attribute(self.env, req.authname, |
---|
528 | attribute='email') |
---|
529 | email = req.authname in attributes and \ |
---|
530 | attributes[req.authname][1].get('email') or None |
---|
531 | new_email = req.args.get('email', '').strip() |
---|
532 | if (email or new_email) and email != new_email: |
---|
533 | # Attempt to change email to an empty or invalid |
---|
534 | # address detected, resetting to previously stored value. |
---|
535 | req.redirect(req.href.prefs(None)) |
---|
536 | if self.verify_email and handler is not self and \ |
---|
537 | 'email_verification_token' in req.session and \ |
---|
538 | not req.perm.has_permission('ACCTMGR_ADMIN'): |
---|
539 | # TRANSLATOR: Your permissions have been limited until you ... |
---|
540 | link = tag.a(_("verify your email address"), |
---|
541 | href=req.href.verify_email() |
---|
542 | ) |
---|
543 | # TRANSLATOR: ... verify your email address |
---|
544 | chrome.add_warning(req, Markup(tag.span(Markup(_( |
---|
545 | "Your permissions have been limited until you %(link)s.", |
---|
546 | link=link)))) |
---|
547 | ) |
---|
548 | req.perm = perm.PermissionCache(self.env, 'anonymous') |
---|
549 | return handler |
---|
550 | |
---|
551 | def post_process_request(self, req, template, data, content_type): |
---|
552 | if not req.session.authenticated: |
---|
553 | # Don't start the email verification procedure on anonymous users. |
---|
554 | return template, data, content_type |
---|
555 | |
---|
556 | email = req.session.get('email') |
---|
557 | # Only send verification if the user entered an email address. |
---|
558 | if self.verify_email and self.email_enabled is True and email and \ |
---|
559 | email != req.session.get('email_verification_sent_to') and \ |
---|
560 | not req.perm.has_permission('ACCTMGR_ADMIN'): |
---|
561 | req.session['email_verification_token'] = self._gen_token() |
---|
562 | req.session['email_verification_sent_to'] = email |
---|
563 | AccountManager(self.env)._notify( |
---|
564 | 'email_verification_requested', |
---|
565 | req.authname, |
---|
566 | req.session['email_verification_token'] |
---|
567 | ) |
---|
568 | # TRANSLATOR: An email has been sent to <%(email)s> |
---|
569 | # with a token to ... (the link label for following message) |
---|
570 | link = tag.a(_("verify your new email address"), |
---|
571 | href=req.href.verify_email() |
---|
572 | ) |
---|
573 | # TRANSLATOR: ... verify your new email address |
---|
574 | chrome.add_notice(req, Markup(tag.span(Markup(_( |
---|
575 | """An email has been sent to <%(email)s> with a token to |
---|
576 | %(link)s.""", email=email, link=link)))) |
---|
577 | ) |
---|
578 | return template, data, content_type |
---|
579 | |
---|
580 | # IRequestHandler methods |
---|
581 | |
---|
582 | def match_request(self, req): |
---|
583 | return req.path_info == '/verify_email' |
---|
584 | |
---|
585 | def process_request(self, req): |
---|
586 | if not req.session.authenticated: |
---|
587 | chrome.add_warning(req, Markup(tag.span(tag_( |
---|
588 | "Please log in to finish email verification procedure."))) |
---|
589 | ) |
---|
590 | req.redirect(req.href.login()) |
---|
591 | if 'email_verification_token' not in req.session: |
---|
592 | chrome.add_notice(req, _("Your email is already verified.")) |
---|
593 | elif req.method == 'POST' and 'resend' in req.args: |
---|
594 | AccountManager(self.env)._notify( |
---|
595 | 'email_verification_requested', |
---|
596 | req.authname, |
---|
597 | req.session['email_verification_token'] |
---|
598 | ) |
---|
599 | chrome.add_notice(req, |
---|
600 | _("A notification email has been resent to <%s>."), |
---|
601 | req.session.get('email') |
---|
602 | ) |
---|
603 | elif 'verify' in req.args: |
---|
604 | # allow via POST or GET (the latter for email links) |
---|
605 | if req.args['token'] == req.session['email_verification_token']: |
---|
606 | del req.session['email_verification_token'] |
---|
607 | chrome.add_notice( |
---|
608 | req, _("Thank you for verifying your email address.") |
---|
609 | ) |
---|
610 | req.redirect(req.href.prefs()) |
---|
611 | else: |
---|
612 | chrome.add_warning(req, _("Invalid verification token")) |
---|
613 | data = {'_dgettext': dgettext} |
---|
614 | if 'token' in req.args: |
---|
615 | data['token'] = req.args['token'] |
---|
616 | if 'email_verification_token' not in req.session: |
---|
617 | data['button_state'] = { 'disabled': 'disabled' } |
---|
618 | return 'verify_email.html', data, None |
---|
619 | |
---|
620 | def _gen_token(self): |
---|
621 | return base64.urlsafe_b64encode(urandom(6)) |
---|