source: TracAccountManager/0.10/acct_mgr/web_ui.py @ 11

Last change on this file since 11 was 2, checked in by guillaume, 17 years ago

Ajout TracAccountManager en français

File size: 15.0 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
4#
5# "THE BEER-WARE LICENSE" (Revision 42):
6# <trac@matt-good.net> wrote this file.  As long as you retain this notice you
7# can do whatever you want with this stuff. If we meet some day, and you think
8# this stuff is worth it, you can buy me a beer in return.   Matthew Good
9#
10# Author: Matthew Good <trac@matt-good.net>
11
12from __future__ import generators
13
14import random
15import string
16
17from trac import perm, util
18from trac.core import *
19from trac.config import IntOption
20from trac.notification import NotificationSystem, NotifyEmail
21from trac.web import auth
22from trac.web.api import IAuthenticator
23from trac.web.main import IRequestHandler
24from trac.web.chrome import INavigationContributor, ITemplateProvider
25from trac.util import Markup
26
27from api import AccountManager
28
29def _create_user(req, env, check_permissions=True):
30    mgr = AccountManager(env)
31
32    user = req.args.get('user')
33    if not user:
34        raise TracError(u'Le nom d\'utilisateur ne peut être vide.')
35
36    if mgr.has_user(user):
37        raise TracError(u'Un autre coompte avec ce nom existe déjà.')
38
39    if check_permissions:
40        # disallow registration of accounts which have existing permissions
41        permission_system = perm.PermissionSystem(env)
42        if permission_system.get_user_permissions(user) != \
43           permission_system.get_user_permissions('authenticated'):
44            raise TracError(u'Un autre coompte avec ce nom existe déjà.')
45
46    password = req.args.get('password')
47    if not password:
48        raise TracError(u'Le mot de passe ne peut pas être vide.')
49
50    if password != req.args.get('password_confirm'):
51        raise TracError(u'Le mot de passe doit correspondre.')
52
53    mgr.set_password(user, password)
54
55    db = env.get_db_cnx()
56    cursor = db.cursor()
57    cursor.execute("SELECT count(*) FROM session "
58                   "WHERE sid=%s AND authenticated=1",
59                   (user,))
60    exists, = cursor.fetchone()
61    if not exists:
62        cursor.execute("INSERT INTO session "
63                       "(sid, authenticated, last_visit) "
64                       "VALUES (%s, 1, 0)",
65                       (user,))
66
67    for key in ('name', 'email'):
68        value = req.args.get(key)
69        if not value:
70            continue
71        cursor.execute("UPDATE session_attribute SET value=%s "
72                       "WHERE name=%s AND sid=%s AND authenticated=1",
73                       (value, key, user))
74        if not cursor.rowcount:
75            cursor.execute("INSERT INTO session_attribute "
76                           "(sid,authenticated,name,value) "
77                           "VALUES (%s,1,%s,%s)",
78                           (user, key, value))
79    db.commit()
80
81
82class PasswordResetNotification(NotifyEmail):
83    template_name = 'reset_password_email.cs'
84    _username = None
85
86    def get_recipients(self, resid):
87        return ([resid],[])
88
89    def get_smtp_address(self, addr):
90        """Overrides `get_smtp_address` in order to prevent CCing users
91        other than the user whose password is being reset.
92        """
93        if addr == self._username:
94            return NotifyEmail.get_smtp_address(self, addr)
95        else:
96            return None
97
98    def notify(self, username, password):
99        # save the username for use in `get_smtp_address`
100        self._username = username
101        self.hdf['account.username'] = username
102        self.hdf['account.password'] = password
103        self.hdf['login.link'] = self.env.abs_href.login()
104
105        projname = self.config.get('project', 'name')
106        subject = u'[%s] Ré-initialisation du mot de passe pour l\'utilisateur: %s' % (projname, username)
107
108        NotifyEmail.notify(self, username, subject)
109
110
111class AccountModule(Component):
112    """Allows users to change their password, reset their password if they've
113    forgotten it, or delete their account.  The settings for the AccountManager
114    module must be set in trac.ini in order to use this.
115    """
116
117    implements(INavigationContributor, IRequestHandler, ITemplateProvider)
118
119    _password_chars = string.ascii_letters + string.digits
120    password_length = IntOption('account-manager', 'generated_password_length', 8,
121                                u'Longueur des mots de passe aléatoirement générés,'
122                                u' créés à l\'occasion de la ré-initialisation du '
123                                u'mot de passe d\'un compte.')
124
125    def __init__(self):
126        self._write_check(log=True)
127
128    def _write_check(self, log=False):
129        writable = AccountManager(self.env).supports('set_password')
130        if not writable and log:
131            self.log.warn('AccountModule is disabled because the password '
132                          'store does not support writing.')
133        return writable
134
135    #INavigationContributor methods
136    def get_active_navigation_item(self, req):
137        if req.path_info == '/account':
138            return 'account'
139        elif req.path_info == '/reset_password':
140            return 'reset_password'
141
142    def get_navigation_items(self, req):
143        if not self._write_check():
144            return
145        if req.authname != 'anonymous':
146            yield 'metanav', 'account', Markup(u'<a href="%s">Mon Compte</a>',
147                                               (req.href.account()))
148        elif self.reset_password_enabled and not LoginModule(self.env).enabled:
149            yield 'metanav', 'reset_password', Markup(u'<a href="%s">Mot de passe oublié ?</a>',
150                                                      (req.href.reset_password()))
151
152    # IRequestHandler methods
153    def match_request(self, req):
154        return (req.path_info in ('/account', '/reset_password')
155                and self._write_check(log=True))
156
157    def process_request(self, req):
158        if req.path_info == '/account':
159            self._do_account(req)
160            return 'account.cs', None
161        elif req.path_info == '/reset_password':
162            self._do_reset_password(req)
163            return 'reset_password.cs', None
164
165    def reset_password_enabled(self):
166        return (self.env.is_component_enabled(AccountModule)
167                and NotificationSystem(self.env).smtp_enabled
168                and self._write_check())
169    reset_password_enabled = property(reset_password_enabled)
170
171    def _do_account(self, req):
172        if req.authname == 'anonymous':
173            req.redirect(self.env.href.wiki())
174        action = req.args.get('action')
175        delete_enabled = AccountManager(self.env).supports('delete_user')
176        req.hdf['delete_enabled'] = delete_enabled
177        if req.method == 'POST':
178            if action == 'change_password':
179                self._do_change_password(req)
180            elif action == 'delete':
181                self._do_delete(req)
182
183    def _do_reset_password(self, req):
184        if req.authname != 'anonymous':
185            req.hdf['reset.logged_in'] = True
186            req.hdf['account_href'] = req.href.account()
187            return
188        if req.method == 'POST':
189            username = req.args.get('username')
190            email = req.args.get('email')
191            if not username:
192                req.hdf['reset.error'] = u'Nom d\'utilisateur requis'
193                return
194            if not email:
195                req.hdf['reset.error'] = u'Courriel requis'
196                return
197
198            notifier = PasswordResetNotification(self.env)
199
200            if email != notifier.email_map.get(username):
201                req.hdf['reset.error'] = u'Le nom d\'utilisateur et le courriel ' \
202                                         u'ne correspondent pas à un compte connu.'
203                return
204
205            new_password = self._random_password()
206            notifier.notify(username, new_password)
207            AccountManager(self.env).set_password(username, new_password)
208            req.hdf['reset.sent_to_email'] = email
209
210    def _random_password(self):
211        return ''.join([random.choice(self._password_chars)
212                        for _ in xrange(self.password_length)])
213
214    def _do_change_password(self, req):
215        user = req.authname
216        mgr = AccountManager(self.env)
217        old_password = req.args.get('old_password')
218        if not old_password:
219            req.hdf['account.save_error'] = u'L\'ancien mot de passe ne peut pas être vide.'
220            return
221        if not mgr.check_password(user, old_password):
222            req.hdf['account.save_error'] = u'L\'ancien mot de passe n\'est pas correct.'
223            return
224
225        password = req.args.get('password')
226        if not password:
227            req.hdf['account.save_error'] = u'Le mot de passe ne peut pas être vide.'
228            return
229
230        if password != req.args.get('password_confirm'):
231            req.hdf['account.save_error'] = u'Les mots de passe doivent correspondre.'
232            return
233
234        mgr.set_password(user, password)
235        req.hdf['account.message'] = u'Mot de passe mis à jour avec succés.'
236
237    def _do_delete(self, req):
238        user = req.authname
239        mgr = AccountManager(self.env)
240        password = req.args.get('password')
241        if not password:
242            req.hdf['account.delete_error'] = u'Le mot de passe ne peut pas être vide.'
243            return
244        if not mgr.check_password(user, password):
245            req.hdf['account.delete_error'] = u'Le mot de passe n\'est pas correct.'
246            return
247
248        mgr.delete_user(user)
249        req.redirect(self.env.href.logout())
250
251    # ITemplateProvider
252
253    def get_htdocs_dirs(self):
254        """Return the absolute path of a directory containing additional
255        static resources (such as images, style sheets, etc).
256        """
257        return []
258
259    def get_templates_dirs(self):
260        """Return the absolute path of the directory containing the provided
261        ClearSilver templates.
262        """
263        from pkg_resources import resource_filename
264        return [resource_filename(__name__, 'templates')]
265
266
267class RegistrationModule(Component):
268    """Provides users the ability to register a new account.
269    Requires configuration of the AccountManager module in trac.ini.
270    """
271
272    implements(INavigationContributor, IRequestHandler, ITemplateProvider)
273
274    def __init__(self):
275        self._enable_check(log=True)
276
277    def _enable_check(self, log=False):
278        writable = AccountManager(self.env).supports('set_password')
279        ignore_case = auth.LoginModule(self.env).ignore_case
280        if log:
281            if not writable:
282                self.log.warn('RegistrationModule is disabled because the '
283                              'password store does not support writing.')
284            if ignore_case:
285                self.log.warn('RegistrationModule is disabled because '
286                              'ignore_auth_case is enabled in trac.ini.  '
287                              'This setting needs disabled to support '
288                              'registration.')
289        return writable and not ignore_case
290
291    #INavigationContributor methods
292
293    def get_active_navigation_item(self, req):
294        return 'register'
295
296    def get_navigation_items(self, req):
297        if not self._enable_check():
298            return
299        if req.authname == 'anonymous':
300            yield 'metanav', 'register', Markup(u'<a href="%s">S\'enregistrer</a>',
301                                                (self.env.href.register()))
302
303    # IRequestHandler methods
304
305    def match_request(self, req):
306        return req.path_info == '/register' and self._enable_check(log=True)
307
308    def process_request(self, req):
309        if req.authname != 'anonymous':
310            req.redirect(self.env.href.account())
311        action = req.args.get('action')
312        if req.method == 'POST' and action == 'create':
313            try:
314                _create_user(req, self.env)
315            except TracError, e:
316                req.hdf['registration.error'] = e.message
317            else:
318                req.redirect(self.env.href.login())
319        req.hdf['reset_password_enabled'] = \
320            (self.env.is_component_enabled(AccountModule)
321             and NotificationSystem(self.env).smtp_enabled)
322
323        return 'register.cs', None
324
325
326    # ITemplateProvider
327
328    def get_htdocs_dirs(self):
329        """Return the absolute path of a directory containing additional
330        static resources (such as images, style sheets, etc).
331        """
332        return []
333
334    def get_templates_dirs(self):
335        """Return the absolute path of the directory containing the provided
336        ClearSilver templates.
337        """
338        from pkg_resources import resource_filename
339        return [resource_filename(__name__, 'templates')]
340
341
342def if_enabled(func):
343    def wrap(self, *args, **kwds):
344        if not self.enabled:
345            return None
346        return func(self, *args, **kwds)
347    return wrap
348
349
350class LoginModule(auth.LoginModule):
351
352    implements(ITemplateProvider)
353
354    def authenticate(self, req):
355        if req.method == 'POST' and req.path_info.startswith('/login'):
356            req.environ['REMOTE_USER'] = self._remote_user(req)
357        return auth.LoginModule.authenticate(self, req)
358    authenticate = if_enabled(authenticate)
359
360    match_request = if_enabled(auth.LoginModule.match_request)
361
362    def process_request(self, req):
363        if req.path_info.startswith('/login') and req.authname == 'anonymous':
364            req.hdf['referer'] = self._referer(req)
365            if AccountModule(self.env).reset_password_enabled:
366                req.hdf['trac.href.reset_password'] = req.href.reset_password()
367            if req.method == 'POST':
368                req.hdf['login.error'] = u'Utilisateur ou mot de passe invalide'
369            return 'login.cs', None
370        return auth.LoginModule.process_request(self, req)
371
372    def _do_login(self, req):
373        if not req.remote_user:
374            req.redirect(self.env.abs_href())
375        return auth.LoginModule._do_login(self, req)
376
377    def _remote_user(self, req):
378        user = req.args.get('user')
379        password = req.args.get('password')
380        if not user or not password:
381            return None
382        if AccountManager(self.env).check_password(user, password):
383            return user
384        return None
385
386    def _redirect_back(self, req):
387        """Redirect the user back to the URL she came from."""
388        referer = self._referer(req)
389        if referer and not referer.startswith(req.base_url):
390            # don't redirect to external sites
391            referer = None
392        req.redirect(referer or self.env.abs_href())
393
394    def _referer(self, req):
395        return req.args.get('referer') or req.get_header('Referer')
396
397    def enabled(self):
398        # Users should disable the built-in authentication to use this one
399        return not self.env.is_component_enabled(auth.LoginModule)
400    enabled = property(enabled)
401
402    # ITemplateProvider
403
404    def get_htdocs_dirs(self):
405        """Return the absolute path of a directory containing additional
406        static resources (such as images, style sheets, etc).
407        """
408        return []
409
410    def get_templates_dirs(self):
411        """Return the absolute path of the directory containing the provided
412        ClearSilver templates.
413        """
414        from pkg_resources import resource_filename
415        return [resource_filename(__name__, 'templates')]
416
Note: See TracBrowser for help on using the repository browser.