source: captchaauthplugin/0.11/captchaauth/auth.py

Last change on this file was 6612, checked in by Jeff Hammel, 14 years ago

allow user to really login and register

File size: 13.6 KB
Line 
1"""
2plugin for Trac to give anonymous users authenticated access
3using a CAPTCHA
4"""
5
6# Plugin for trac 0.11
7
8import random
9import sys
10import time
11import urllib
12
13from componentdependencies import IRequireComponents
14
15from genshi.builder import Markup
16from genshi.builder import tag
17from genshi.filters.transform import Transformer
18
19from pkg_resources import resource_filename
20
21from skimpyGimpy import skimpyAPI
22
23from trac.core import *
24from trac.db import Table, Column
25from trac.env import IEnvironmentSetupParticipant
26from trac.web import IRequestFilter
27from trac.web import ITemplateStreamFilter
28from trac.web.api import IAuthenticator
29from trac.web.auth import LoginModule
30from trac.web.chrome import add_warning
31from trac.web.chrome import Chrome
32from trac.web.chrome import INavigationContributor
33from trac.web.chrome import ITemplateProvider
34from trac.config import ListOption
35from trac.config import Option
36
37from tracsqlhelper import create_table
38from tracsqlhelper import execute_non_query
39from tracsqlhelper import get_scalar
40from tracsqlhelper import get_table
41from tracsqlhelper import insert_update
42
43from utils import random_word
44from web_ui import ImageCaptcha
45
46try: 
47    from acct_mgr.api import AccountManager
48    from acct_mgr.web_ui import RegistrationModule
49except:
50    AccountManager = None
51
52class AuthCaptcha(Component):
53
54    ### class data
55    implements(IRequestFilter, 
56               ITemplateStreamFilter, 
57               ITemplateProvider, 
58               IAuthenticator, 
59               IEnvironmentSetupParticipant, 
60               IRequireComponents, 
61               INavigationContributor
62               )
63
64    dict_file = Option('captchaauth', 'dictionary_file',
65                           default="http://java.sun.com/docs/books/tutorial/collections/interfaces/examples/dictionary.txt")
66    captcha_type = Option('captchaauth', 'type',
67                          default="png")
68    realms = ListOption('captchaauth', 'realms',
69                        default="wiki, newticket")
70    permissions = { 'wiki': [ 'WIKI_CREATE', 'WIKI_MODIFY' ],
71                    'newticket': [ 'TICKET_CREATE' ] }
72
73    xpath = { 'ticket.html': "//div[@class='buttons']" }
74    delete = { 'ticket.html': "//div[@class='field']" }
75
76    ### IRequestFilter methods
77
78    def pre_process_request(self, req, handler):
79        """Called after initial handler selection, and can be used to change
80        the selected handler or redirect request.
81       
82        Always returns the request handler, even if unchanged.
83        """
84
85        if req.method == 'GET':
86            if req.path_info.strip('/') in ['register', 'login'] and req.authname != 'anonymous':
87                login_module = LoginModule(self.env)
88                login_module._do_logout(req)
89                req.redirect(req.href(req.path_info))
90
91
92        if req.method == 'POST':
93
94            realm = self.realm(req)
95
96            # set the session data for name and email if CAPTCHA-authenticated
97            if 'captchaauth' in req.args:
98                name, email = self.identify(req)
99                for field in 'name', 'email':
100                    value = locals()[field]
101                    if value:
102                        req.session[field] = value
103                req.session.save()
104                if req.authname != 'anonymous' and realm == 'newticket':
105                    req.args['author'] = name
106                    if email:
107                        req.args['author'] += ' <%s>' % email
108
109            # redirect anonymous user posts that are not CAPTCHA-identified
110            if req.authname == 'anonymous' and realm in self.realms:
111               
112                if 'captchaauth' in req.args and 'captchaid' in req.args:
113                    # add warnings from CAPTCHA authentication
114                    captcha = self.captcha(req)
115                    if req.args['captchaauth'] != captcha:
116                        add_warning(req, "You typed the wrong word. Please try again.")
117                        try:
118                            # delete used CAPTCHA
119                            execute_non_query(self.env, "DELETE FROM captcha WHERE id=%s", req.args['captchaid'])
120                        except:
121                            pass
122
123                    name, email = self.identify(req)
124                    if not name:
125                        add_warning(req, 'Please provide your name')
126                    if AccountManager and name in AccountManager(self.env).get_users():
127                        add_warning(req, '%s is already taken as by a registered user.  Please login or use a different name' % name)
128
129                # redirect to previous location
130                location = req.get_header('referer')
131                if location:
132                    location, query = urllib.splitquery(location)
133                   
134                    if realm == 'newticket':
135                        args = [(key.split('field_',1)[-1], value)
136                                for key, value in req.args.items()
137                                if key.startswith('field_')]
138                        location += '?%s' % urllib.urlencode(args)
139                else:
140                    location =  req.href()
141                req.redirect(location)
142
143        return handler
144
145    # for ClearSilver templates
146    def post_process_request(self, req, template, content_type):
147        """Do any post-processing the request might need; typically adding
148        values to req.hdf, or changing template or mime type.
149       
150        Always returns a tuple of (template, content_type), even if
151        unchanged.
152
153        Note that `template`, `content_type` will be `None` if:
154         - called when processing an error page
155         - the default request handler did not return any result
156
157        (for 0.10 compatibility; only used together with ClearSilver templates)
158        """
159        return (template, content_type)
160
161    # for Genshi templates
162    def post_process_request(self, req, template, data, content_type):
163        """Do any post-processing the request might need; typically adding
164        values to the template `data` dictionary, or changing template or
165        mime type.
166       
167        `data` may be update in place.
168
169        Always returns a tuple of (template, data, content_type), even if
170        unchanged.
171
172        Note that `template`, `data`, `content_type` will be `None` if:
173         - called when processing an error page
174         - the default request handler did not return any result
175
176        (Since 0.11)
177        """
178        return (template, data, content_type)
179
180   
181    ### ITemplateStreamFilter method
182
183    def filter_stream(self, req, method, filename, stream, data):
184        """Return a filtered Genshi event stream, or the original unfiltered
185        stream if no match.
186
187        `req` is the current request object, `method` is the Genshi render
188        method (xml, xhtml or text), `filename` is the filename of the template
189        to be rendered, `stream` is the event stream and `data` is the data for
190        the current template.
191
192        See the Genshi documentation for more information.
193        """
194
195        # only show CAPTCHAs for anonymous users
196        if req.authname != 'anonymous':
197            return stream
198
199        # only put CAPTCHAs in the realms specified
200        realm = self.realm(req)
201        if realm not in self.realms:
202            return stream
203
204        # add the CAPTCHA to the stream
205        if filename in self.xpath:
206
207            # store CAPTCHA in DB and session
208            word = random_word(self.dict_file)
209            insert_update(self.env, 'captcha', 'id', req.session.sid, dict(word=word))
210            req.session['captcha'] = word
211            req.session.save()
212
213            # render the template
214            chrome = Chrome(self.env)
215            template = chrome.load_template('captcha.html')
216            _data = {}
217
218            # CAPTCHA type
219            if self.captcha_type == 'png':
220                captcha = tag.img(None, src=req.href('captcha.png'))
221            else:
222                captcha = Markup(skimpyAPI.Pre(word).data())
223
224            _data['captcha'] = captcha
225            _data['email'] = req.session.get('email', '')
226            _data['name'] = req.session.get('name', '')
227            _data['captchaid'] = req.session.sid
228            xpath = self.xpath[filename]
229            stream |= Transformer(xpath).before(template.generate(**_data))
230            if filename in self.delete:
231                stream |= Transformer(self.delete[filename]).remove()
232
233        return stream
234
235    ### methods for ITemplateProvider
236
237    """Extension point interface for components that provide their own
238    ClearSilver templates and accompanying static resources.
239    """
240
241    def get_htdocs_dirs(self):
242        """Return a list of directories with static resources (such as style
243        sheets, images, etc.)
244
245        Each item in the list must be a `(prefix, abspath)` tuple. The
246        `prefix` part defines the path in the URL that requests to these
247        resources are prefixed with.
248       
249        The `abspath` is the absolute path to the directory containing the
250        resources on the local file system.
251        """
252        return []
253
254    def get_templates_dirs(self):
255        """Return a list of directories containing the provided template
256        files.
257        """
258        return [resource_filename(__name__, 'templates')]
259
260    ### method for IAuthenticator
261
262    """Extension point interface for components that can provide the name
263    of the remote user."""
264
265    def authenticate(self, req):
266        """Return the name of the remote user, or `None` if the identity of the
267        user is unknown."""
268
269        # check for an authenticated user
270        login_module = LoginModule(self.env)
271        remote_user = login_module.authenticate(req)
272        if remote_user:
273            return remote_user
274
275        # authenticate via a CAPTCHA
276        if 'captchaauth' in req.args and 'captchaid' in req.args:
277
278            # ensure CAPTCHA identification
279            captcha = self.captcha(req)
280            if captcha != req.args['captchaauth']:
281                return 
282
283            # ensure sane identity
284            name, email = self.identify(req)
285            if name is None:
286                return
287            if AccountManager and name in AccountManager(self.env).get_users():
288                return
289
290            # delete used CAPTCHA on success
291            try:
292                execute_non_query(self.env, "DELETE FROM captcha WHERE id=%s", req.args['captchaid'])
293            except:
294                pass
295
296            # log the user in
297            req.environ['REMOTE_USER'] = name
298            login_module._do_login(req)
299
300    ### methods for INavigationContributor
301
302    """Extension point interface for components that contribute items to the
303    navigation.
304    """
305
306    def get_active_navigation_item(self, req):
307        """This method is only called for the `IRequestHandler` processing the
308        request.
309       
310        It should return the name of the navigation item that should be
311        highlighted as active/current.
312        """
313        return None
314
315    def get_navigation_items(self, req):
316        """Should return an iterable object over the list of navigation items to
317        add, each being a tuple in the form (category, name, text).
318        """
319        if req.authname != 'anonymous' and 'captcha' in req.session:
320            return [('metanav', '_login', tag.a("Login", 
321                                               href=req.href.login())),
322                    ('metanav', '_register', tag.a("Register",
323                                                  href=req.href.register()))]
324        return []
325
326    ### methods for IEnvironmentSetupParticipant
327
328    """Extension point interface for components that need to participate in the
329    creation and upgrading of Trac environments, for example to create
330    additional database tables."""
331
332    def environment_created(self):
333        """Called when a new Trac environment is created."""
334        if self.environment_needs_upgrade(None):
335            self.upgrade_environment(None)
336
337    def environment_needs_upgrade(self, db):
338        """Called when Trac checks whether the environment needs to be upgraded.
339       
340        Should return `True` if this participant needs an upgrade to be
341        performed, `False` otherwise.
342        """
343        try:
344            get_table(self.env, 'captcha')
345        except:
346            return True
347        return False
348
349    def upgrade_environment(self, db):
350        """Actually perform an environment upgrade.
351       
352        Implementations of this method should not commit any database
353        transactions. This is done implicitly after all participants have
354        performed the upgrades they need without an error being raised.
355        """
356
357        # table of CAPTCHAs
358        captcha_table = Table('captcha', key='key')[
359            Column('id'),
360            Column('word')]
361        create_table(self.env, captcha_table)
362
363    ### method for IRequireComponents
364       
365    def requires(self):
366        """list of component classes that this component depends on"""
367        return [ImageCaptcha]
368
369
370    ### internal methods
371
372    def identify(self, req):
373        """
374        identify the user, ensuring uniqueness (TODO);
375        returns a tuple of (name, email) or success or None
376        """
377        name = req.args.get('name', None).strip()
378        email = req.args.get('email', None).strip()
379        return name, email
380       
381
382    def realm(self, req):
383        """
384        returns the realm according to the request
385        """
386        path = req.path_info.strip('/').split('/')
387        if not path:
388            return
389        # TODO: default handler ('/')
390        return path[0]
391
392    def captcha(self, req):
393        return get_scalar(self.env, "SELECT word FROM captcha WHERE id=%s", 0, req.args['captchaid'])
Note: See TracBrowser for help on using the repository browser.