Edgewall Software

Ticket #791: core.py

File core.py, 19.2 KB (added by shawn.debnath at sun.com, 4 years ago)

Added patched up 0.8.1 core.py, download and enjoy!

Line 
1# -*- coding: iso8859-1 -*-
2#
3# Copyright (C) 2003, 2004 Edgewall Software
4# Copyright (C) 2003, 2004 Jonas Borgstr�jonas@edgewall.com>
5#
6# Trac is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License as
8# published by the Free Software Foundation; either version 2 of the
9# License, or (at your option) any later version.
10#
11# Trac is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19#
20# Author: Jonas Borgstr�jonas@edgewall.com>
21
22import os
23import re
24import sys
25import cgi
26import time
27import locale
28import urllib
29import warnings
30import util
31from types import ListType
32
33import Href
34import perm
35import auth
36import authzperm
37import Environment
38import Session
39
40from util import sql_to_hdf, TracError
41from __init__ import __version__
42
43warnings.filterwarnings('ignore', 'DB-API extension cursor.next() used')
44
45modules = {
46#    name           (module_name, class_name, requires_svn)
47    'log'         : ('Log', 'Log', 1),
48    'file'        : ('File', 'File', 1),
49    'wiki'        : ('Wiki', 'WikiModule', 0),
50    'about_trac'  : ('About', 'About', 0),
51    'search'      : ('Search', 'Search', 0),
52    'report'      : ('Report', 'Report', 0),
53    'ticket'      : ('Ticket', 'TicketModule', 0),
54    'browser'     : ('Browser', 'Browser', 1),
55    'timeline'    : ('Timeline', 'Timeline', 1),
56    'changeset'   : ('Changeset', 'Changeset', 1),
57    'newticket'   : ('Ticket', 'NewticketModule', 0),
58    'query'       : ('Query', 'QueryModule', 0),
59    'attachment'  : ('File', 'Attachment', 0),
60    'roadmap'     : ('Roadmap', 'Roadmap', 0),
61    'settings'    : ('Settings', 'Settings', 0),
62    'milestone'   : ('Milestone', 'Milestone', 0)
63    }
64
65class TracFieldStorage(cgi.FieldStorage):
66    """
67    FieldStorage class with a few more functions to make it behave a bit
68    more like a dictionary
69    """
70    get = cgi.FieldStorage.getvalue
71
72    def __setitem__(self, name, value):
73        if self.has_key(name):
74            del self[name]
75        self.list.append(cgi.MiniFieldStorage(name, value))
76
77    def __delitem__(self, name):
78        if not self.has_key(name):
79            raise KeyError(name)
80        self.list = filter(lambda x, name=name: x.name != name, self.list)
81
82
83def parse_path_info(args, path_info):
84    def set_if_missing(fs, name, value):
85        if value and not fs.has_key(name):
86            fs.list.append(cgi.MiniFieldStorage(name, value))
87
88    if not path_info or path_info in ['/login', '/logout']:
89        return args
90    match = re.search('^/(about_trac|wiki)(?:/(.*))?', path_info)
91    if match:
92        set_if_missing(args, 'mode', match.group(1))
93        if match.group(2):
94            set_if_missing(args, 'page', match.group(2))
95        return args
96    match = re.search('^/(newticket|timeline|search|roadmap|settings|query)/?', path_info)
97    if match:
98        set_if_missing(args, 'mode', match.group(1))
99        return args
100    match = re.search('^/(ticket|report)(?:/([0-9]+)/*)?', path_info)
101    if match:
102        set_if_missing(args, 'mode', match.group(1))
103        if match.group(2):
104            set_if_missing(args, 'id', match.group(2))
105        return args
106    match = re.search('^/(browser|log|file)(?:(/.*))?', path_info)
107    if match:
108        set_if_missing(args, 'mode', match.group(1))
109        if match.group(2):
110            set_if_missing(args, 'path', match.group(2))
111        return args
112    match = re.search('^/changeset/([0-9]+)/?', path_info)
113    if match:
114        set_if_missing(args, 'mode', 'changeset')
115        set_if_missing(args, 'rev', match.group(1))
116        return args
117    match = re.search('^/attachment/([a-zA-Z_]+)/([^/]+)(?:/(.*)/?)?', path_info)
118    if match:
119        set_if_missing(args, 'mode', 'attachment')
120        set_if_missing(args, 'type', match.group(1))
121        set_if_missing(args, 'id', urllib.unquote(match.group(2)))
122        set_if_missing(args, 'filename', match.group(3))
123        return args
124    match = re.search('^/milestone(?:/([^\?]+))?(?:/(.*)/?)?', path_info)
125    if match:
126        set_if_missing(args, 'mode', 'milestone')
127        if match.group(1):
128            set_if_missing(args, 'id', urllib.unquote(match.group(1)))
129        return args
130    return args
131
132def parse_args(command, path_info, query_string,
133               fp=None, env = None, _headers=None):
134    if not env:
135        env = {'REQUEST_METHOD': command, 'QUERY_STRING': query_string}
136    if command in ['GET', 'HEAD']:
137        _headers = None
138    args = TracFieldStorage(fp, environ=env, headers=_headers, keep_blank_values=1)
139    parse_path_info(args, path_info)
140    return args
141
142def add_args_to_hdf(args, hdf):
143    for key in args.keys():
144        if not key:
145            continue
146        if type(args[key]) is not ListType:
147            hdf.setValue('args.%s' % key, str(args[key].value))
148        else:
149            for i in range(len(args[key])):
150                hdf.setValue('args.%s.%d' % (key, i), str(args[key][i].value))
151
152def module_factory(args, env, db, req):
153    mode = args.get('mode', 'wiki')
154    module_name, constructor_name, need_svn = modules[mode]
155    module = __import__(module_name, globals(),  locals())
156    constructor = getattr(module, constructor_name)
157    module = constructor()
158    module.pool = None
159    module.args = args
160    module.env = env
161    module.log = env.log
162    module.req = req
163    module._name = mode
164    module.db = db
165    module.perm = perm.PermissionCache(module.db, req.authname)
166    module.perm.add_to_hdf(req.hdf)
167    module.authzperm = None
168
169    # Only open the subversion repository for the modules that really
170    # need it. This saves us some precious time.
171    if need_svn:
172        import sync
173        module.authzperm = authzperm.AuthzPermission(env,req.authname)
174        repos_dir = env.get_config('trac', 'repository_dir')
175        pool, rep, fs_ptr = open_svn_repos(repos_dir)
176        module.repos = rep
177        module.fs_ptr = fs_ptr
178        sync.sync(module.db, rep, fs_ptr, pool)
179        module.pool = pool
180    return module
181
182def open_environment():
183    env_path = os.getenv('TRAC_ENV')
184    if not env_path:
185        raise EnvironmentError, \
186              'Missing environment variable "TRAC_ENV". Trac ' \
187              'requires this variable to point to a valid Trac Environment.'
188
189    env = Environment.Environment(env_path)
190    version = env.get_version()
191    if version < Environment.db_version:
192        raise TracError('The Trac Environment needs to be upgraded. '
193                        'Run "trac-admin %s upgrade"' % env_path)
194    elif version > Environment.db_version:
195        raise TracError('Unknown Trac Environment version (%d).' % version)
196    return env
197
198class RedirectException(Exception):
199    pass
200
201def populate_hdf(hdf, env, db, req):
202    sql_to_hdf(db, "SELECT name FROM enum WHERE type='priority' "
203               "ORDER BY value", hdf, 'enums.priority')
204    sql_to_hdf(db, "SELECT name FROM enum WHERE type='severity' "
205               "ORDER BY value", hdf, 'enums.severity')
206
207    htdocs_location = env.get_config('trac', 'htdocs_location')
208    if htdocs_location[-1] != '/':
209        htdocs_location += '/'
210    hdf.setValue('htdocs_location', htdocs_location)
211    hdf.setValue('project.name', env.get_config('project', 'name'))
212    # Kludges for RSS, etc
213    hdf.setValue('project.name.encoded',
214                 util.escape(env.get_config('project', 'name')))
215    hdf.setValue('project.descr', env.get_config('project', 'descr'))
216    hdf.setValue('project.footer', env.get_config('project', 'footer',
217                  ' Visit the Trac open source project at<br />'
218                  '<a href="http://trac.edgewall.com/">http://trac.edgewall.com/</a>'))
219    hdf.setValue('project.url', env.get_config('project', 'url'))
220    hdf.setValue('trac.href.wiki', env.href.wiki())
221    hdf.setValue('trac.href.browser', env.href.browser('/'))
222    hdf.setValue('trac.href.timeline', env.href.timeline())
223    hdf.setValue('trac.href.roadmap', env.href.roadmap())
224    hdf.setValue('trac.href.report', env.href.report())
225    hdf.setValue('trac.href.query', env.href.query())
226    hdf.setValue('trac.href.newticket', env.href.newticket())
227    hdf.setValue('trac.href.search', env.href.search())
228    hdf.setValue('trac.href.about', env.href.about())
229    hdf.setValue('trac.href.about_config', env.href.about('config'))
230    hdf.setValue('trac.href.login', env.href.login())
231    hdf.setValue('trac.href.logout', env.href.logout())
232    hdf.setValue('trac.href.settings', env.href.settings())
233    hdf.setValue('trac.href.homepage', 'http://trac.edgewall.com/')
234    hdf.setValue('trac.version', __version__)
235    hdf.setValue('trac.time', time.strftime('%c', time.localtime()))
236    hdf.setValue('trac.time.gmt', time.strftime('%a, %d %b %Y %H:%M:%S GMT',
237                                                time.gmtime()))
238
239    hdf.setValue('header_logo.link', env.get_config('header_logo', 'link'))
240    hdf.setValue('header_logo.alt', env.get_config('header_logo', 'alt'))
241    src = env.get_config('header_logo', 'src')
242    src_abs = re.match(r'https?://', src) != None
243    if not src[0] == '/' and not src_abs:
244        src = htdocs_location + src
245    hdf.setValue('header_logo.src', src)
246    hdf.setValue('header_logo.src_abs', str(src_abs))
247    hdf.setValue('header_logo.width', env.get_config('header_logo', 'width'))
248    hdf.setValue('header_logo.height', env.get_config('header_logo', 'height'))
249    hdf.setValue('trac.href.logout', env.href.logout())
250    if req:
251        hdf.setValue('cgi_location', req.cgi_location)
252        hdf.setValue('trac.authname', util.escape(req.authname))
253
254    templates_dir = env.get_config('trac', 'templates_dir')
255    hdf.setValue('hdf.loadpaths.0', env.get_templates_dir())
256    hdf.setValue('hdf.loadpaths.1', templates_dir)
257
258
259class Request:
260    """
261    This class is used to abstract the interface between different frontends.
262
263    Trac modules must use this interface. It is not allowed to have
264    frontend (cgi, tracd, mod_python) specific code in the modules.
265    """
266
267    command = None
268    hdf = None
269    session = None
270
271    def init_request(self):
272        import neo_cgi
273        # The following line is needed so that ClearSilver can be loaded when
274        # we are being run in multiple interpreters under mod_python
275        neo_cgi.update()
276        import neo_cs
277        import neo_util
278        import Cookie
279        self.hdf = neo_util.HDF()
280        self.incookie = Cookie.SimpleCookie()
281        self.outcookie = Cookie.SimpleCookie()
282
283    def get_header(self, name):
284        raise RuntimeError, 'Virtual method not implemented'
285
286    def send_response(self, code):
287        raise RuntimeError, 'Virtual method not implemented'
288
289    def send_header(self, name, value):
290        raise RuntimeError, 'Virtual method not implemented'
291
292    def end_headers(self):
293        raise RuntimeError, 'Virtual method not implemented'
294
295    def send_cookie(self, cookie):
296        cookie = cookie.output(header='')
297        if len(cookie):
298            self.send_header('Set-Cookie', cookie)
299
300    def send_cache_headers(self):
301        self.send_header('Pragma', 'no-cache')
302        self.send_header('Cache-control', 'no-cache')
303        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
304
305    def reauthorize(self):
306        self.send_response(401)
307        self.send_header('WWW-Authenticate', 'Basic realm="Trac"')
308        self.send_cookie(self.outcookie)
309        self.end_headers()
310        self.write('Reauthorizing...')
311
312    def redirect(self, url):
313        self.send_response(302)
314        self.send_header('Location', url)
315        self.send_header('Content-Type', 'text/plain')
316        self.send_cache_headers()
317        self.send_cookie(self.outcookie)
318        self.end_headers()
319        self.write('Redirecting...')
320        raise RedirectException()
321
322    def display(self, cs, content_type='text/html', response=200):
323        import neo_cgi
324        # The following line is needed so that ClearSilver can be loaded when
325        # we are being run in multiple interpreters under mod_python
326        neo_cgi.update()
327        import neo_cs
328        import neo_util
329        if type(cs) == type(''):
330            filename = cs
331            cs = neo_cs.CS(self.hdf)
332            cs.parseFile(filename)
333        data = cs.render()
334        self.send_response(response)
335        self.send_cache_headers()
336        self.send_header('Content-Type', content_type + ';charset=utf-8')
337        self.send_header('Content-Length', len(data))
338        self.send_cookie(self.outcookie)
339        self.end_headers()
340        if self.command != 'HEAD':
341            self.write(data)
342
343    def read(self, len):
344        assert 0
345
346    def write(self, data):
347        assert 0
348
349class CGIRequest(Request):
350    def init_request(self):
351        Request.init_request(self)
352
353        self.cgi_location = os.getenv('SCRIPT_NAME')
354        self.remote_addr = os.getenv('REMOTE_ADDR')
355        self.remote_user = os.getenv('REMOTE_USER')
356        self.command = os.getenv('REQUEST_METHOD')
357        host = os.getenv('SERVER_NAME')
358        proto_port = ''
359        port = int(os.environ.get('SERVER_PORT', 80))
360
361        if os.getenv('HTTPS') in ('on', '1'):
362            # when you support Apache's way, you get it 60% right
363            proto  = 'https'
364            if port != 443:
365               proto_port = ':%d' % port
366        elif port == 443:
367            proto = 'https'
368        else:
369           proto = 'http'
370           if port != 80:
371               proto_port = ':%d' % port
372
373        if os.getenv('HTTP_X_FORWARDED_HOST'):
374            self.base_url = '%s://%s%s/' % (proto,
375                                            os.getenv('HTTP_X_FORWARDED_HOST'),
376                                            self.cgi_location)
377        else:
378            self.base_url = '%s://%s%s%s' % (proto, host, proto_port, self.cgi_location)
379
380        if os.getenv('HTTP_COOKIE'):
381            self.incookie.load(os.getenv('HTTP_COOKIE'))
382        if os.getenv('HTTP_HOST'):
383            self.hdf.setValue('HTTP.Host', os.getenv('HTTP_HOST'))
384        if os.getenv('PATH_INFO'):
385            self.hdf.setValue('HTTP.PathInfo', os.getenv('PATH_INFO'))
386
387        self.hdf.setValue('HTTP.Protocol', proto)
388        if proto_port:
389            self.hdf.setValue('HTTP.Port', str(port))
390
391    def read(self, len):
392        return sys.stdin.read(len)
393
394    def write(self, data):
395        return sys.stdout.write(data)
396
397    def get_header(self, name):
398        return os.getenv('HTTP_' + re.sub('-', '_', name.upper()))
399
400    def send_response(self, code):
401        self.write('Status: %d\r\n' % code)
402
403    def send_header(self, name, value):
404        self.write('%s: %s\r\n' % (name, value))
405
406    def end_headers(self):
407        self.write('\r\n')
408
409def dispatch_request(path_info, args, req, env, database=None):
410    import Wiki
411
412    if not database:
413        database = env.get_db_cnx()
414
415    # Let the wiki module build a dictionary of all page names
416    Wiki.populate_page_dict(database, env)
417
418    authenticator = auth.Authenticator(database, req)
419    logged_out = False
420    if path_info == '/logout':
421        authenticator.logout(req)
422        referer = req.get_header('Referer')
423        if referer and referer[0:len(req.base_url)] != req.base_url:
424            # only redirect to referer if the latter is from the same instance
425            referer = None
426        try:
427            req.redirect(referer or env.href.wiki())
428        except RedirectException:
429            pass
430    elif req.remote_user and authenticator.authname == 'anonymous':
431                logged_out = authenticator.login(req)
432    if path_info == '/login':
433                if logged_out:
434                        req.reauthorize()
435                        return
436                else:
437                        referer = req.get_header('Referer')
438                        if not referer.startswith(req.base_url):
439                        # only redirect to referer if the latter is from
440                        #the same instance
441                                referer = env.href.wiki()
442                        try:
443                                req.redirect(referer)
444                        except RedirectException:
445                                pass
446    req.authname = authenticator.authname
447
448    newsession = args.has_key('newsession') and args['newsession']
449    req.session = Session.Session(env, req, newsession)
450
451    add_args_to_hdf(args, req.hdf)
452    try:
453        pool = None
454        # Load the selected module
455        module = module_factory(args, env, database, req)
456        pool = module.pool
457        module.run()
458    finally:
459        # We do this even if the cgi will terminate directly after. A pool
460        # destruction might trigger important clean-up functions.
461        if pool:
462            import svn.core
463            svn.core.svn_pool_destroy(pool)
464
465def open_svn_repos(repos_dir):
466    from svn import util, repos, core
467
468    core.apr_initialize()
469    pool = core.svn_pool_create(None)
470    # Remove any trailing slash or else subversion might abort
471    if not os.path.split(repos_dir)[1]:
472        repos_dir = os.path.split(repos_dir)[0]
473
474    rep = repos.svn_repos_open(repos_dir, pool)
475    fs_ptr = repos.svn_repos_fs(rep)
476    return pool, rep, fs_ptr
477
478def send_pretty_error(e, env, req=None):
479    import util
480    import Href
481    import os.path
482    import traceback
483    import StringIO
484    tb = StringIO.StringIO()
485    traceback.print_exc(file=tb)
486    if not req:
487        req = CGIRequest()
488        req.authname = ''
489        req.init_request()
490    try:
491        if not env:
492            env = open_environment()
493        env.href = Href.Href(req.cgi_location)
494        cnx = env.get_db_cnx()
495        populate_hdf(req.hdf, env, cnx, req)
496
497        if isinstance(e, util.TracError):
498            req.hdf.setValue('title', e.title or 'Error')
499            req.hdf.setValue('error.title', e.title or 'Error')
500            req.hdf.setValue('error.type', 'TracError')
501            req.hdf.setValue('error.message', e.message)
502            if e.show_traceback:
503                req.hdf.setValue('error.traceback', util.escape(tb.getvalue()))
504        elif isinstance(e, perm.PermissionError):
505            req.hdf.setValue('title', 'Permission Denied')
506            req.hdf.setValue('error.type', 'permission')
507            req.hdf.setValue('error.action', e.action)
508            req.hdf.setValue('error.message', str(e))
509        else:
510            req.hdf.setValue('title', 'Oops')
511            req.hdf.setValue('error.type', 'internal')
512            req.hdf.setValue('error.message', util.escape(str(e)))
513            req.hdf.setValue('error.traceback', util.escape(tb.getvalue()))
514        req.display('error.cs', response=500)
515    except Exception:
516        req.send_response(500)
517        req.send_header('Content-Type', 'text/plain')
518        req.end_headers()
519        req.write('Oops...\n\nTrac detected an internal error:\n\n')
520        req.write(str(e))
521        req.write('\n')
522        req.write(tb.getvalue())
523    if env and env.log != None:
524        env.log.error(str(e))
525        env.log.error(tb.getvalue())
526
527def real_cgi_start():
528
529    env = open_environment()
530
531    req = CGIRequest()
532    req.init_request()
533
534    env.href = Href.Href(req.cgi_location)
535    env.abs_href = Href.Href(req.base_url)
536
537    # Parse arguments
538    path_info = os.getenv('PATH_INFO')
539    args = parse_args(req.command,
540                      path_info, os.getenv('QUERY_STRING'),
541                      sys.stdin, os.environ)
542    dispatch_request(path_info, args, req, env)
543
544def cgi_start():
545    try:
546        locale.setlocale(locale.LC_ALL, '')
547        real_cgi_start()
548    except Exception, e:
549        send_pretty_error(e, None)