39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2005 Edgewall Software
|
|
4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
|
|
5 # Copyright (C) 2005 Matthew Good <trac@matt-good.net>
|
|
6 # All rights reserved.
|
|
7 #
|
|
8 # This software is licensed as described in the file COPYING, which
|
|
9 # you should have received as part of this distribution. The terms
|
|
10 # are also available at http://trac.edgewall.com/license.html.
|
|
11 #
|
|
12 # This software consists of voluntary contributions made by many
|
|
13 # individuals. For the exact contribution history, see the revision
|
|
14 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
15 #
|
|
16 # Author: Christopher Lenz <cmlenz@gmx.de>
|
|
17 # Matthew Good <trac@matt-good.net>
|
|
18
|
|
19 import locale
|
|
20 import os
|
|
21 import sys
|
|
22 import dircache
|
|
23 import urllib
|
|
24
|
|
25 from trac.config import ExtensionOption, OrderedExtensionsOption
|
|
26 from trac.core import *
|
|
27 from trac.env import open_environment
|
|
28 from trac.perm import PermissionCache, NoPermissionCache, PermissionError
|
|
29 from trac.util import reversed, get_last_traceback
|
|
30 from trac.util.datefmt import format_datetime, http_date
|
|
31 from trac.util.text import to_unicode
|
|
32 from trac.util.markup import Markup
|
|
33 from trac.web.api import *
|
|
34 from trac.web.chrome import Chrome
|
|
35 from trac.web.clearsilver import HDFWrapper
|
|
36 from trac.web.href import Href
|
|
37 from trac.web.session import Session
|
|
38
|
|
39 # Environment cache for multithreaded front-ends:
|
|
40 try:
|
|
41 import threading
|
|
42 except ImportError:
|
|
43 import dummy_threading as threading
|
|
44
|
|
45 env_cache = {}
|
|
46 env_cache_lock = threading.Lock()
|
|
47
|
|
48 def _open_environment(env_path, run_once=False):
|
|
49 if run_once:
|
|
50 return open_environment(env_path)
|
|
51
|
|
52 global env_cache, env_cache_lock
|
|
53 env = None
|
|
54 env_cache_lock.acquire()
|
|
55 try:
|
|
56 if not env_path in env_cache:
|
|
57 env_cache[env_path] = open_environment(env_path)
|
|
58 env = env_cache[env_path]
|
|
59 finally:
|
|
60 env_cache_lock.release()
|
|
61
|
|
62 # Re-parse the configuration file if it changed since the last the time it
|
|
63 # was parsed
|
|
64 env.config.parse_if_needed()
|
|
65
|
|
66 return env
|
|
67
|
|
68 def populate_hdf(hdf, env, req=None):
|
|
69 """Populate the HDF data set with various information, such as common URLs,
|
|
70 project information and request-related information.
|
|
71 FIXME: do we really have req==None at times?
|
|
72 """
|
|
73 from trac import __version__
|
|
74 hdf['trac'] = {
|
|
75 'version': __version__,
|
|
76 'time': format_datetime(),
|
|
77 'time.gmt': http_date()
|
|
78 }
|
|
79 hdf['project'] = {
|
|
80 'name': env.project_name,
|
|
81 'name_encoded': env.project_name,
|
|
82 'descr': env.project_description,
|
|
83 'footer': Markup(env.project_footer),
|
|
84 'url': env.project_url
|
|
85 }
|
|
86
|
|
87 if req:
|
|
88 hdf['trac.href'] = {
|
|
89 'wiki': req.href.wiki(),
|
|
90 'browser': req.href.browser('/'),
|
|
91 'timeline': req.href.timeline(),
|
|
92 'roadmap': req.href.roadmap(),
|
|
93 'milestone': req.href.milestone(None),
|
|
94 'report': req.href.report(),
|
|
95 'query': req.href.query(),
|
|
96 'newticket': req.href.newticket(),
|
|
97 'search': req.href.search(),
|
|
98 'about': req.href.about(),
|
|
99 'about_config': req.href.about('config'),
|
|
100 'login': req.href.login(),
|
|
101 'logout': req.href.logout(),
|
|
102 'settings': req.href.settings(),
|
|
103 'homepage': 'http://trac.edgewall.com/'
|
|
104 }
|
|
105
|
|
106 hdf['base_url'] = req.base_url
|
|
107 hdf['base_host'] = req.base_url[:req.base_url.rfind(req.base_path)]
|
|
108 hdf['cgi_location'] = req.base_path
|
|
109 hdf['trac.authname'] = req.authname
|
|
110
|
|
111 if req.perm:
|
|
112 for action in req.perm.permissions():
|
|
113 req.hdf['trac.acl.' + action] = True
|
|
114
|
|
115 for arg in [k for k in req.args.keys() if k]:
|
|
116 if isinstance(req.args[arg], (list, tuple)):
|
|
117 hdf['args.%s' % arg] = [v for v in req.args[arg]]
|
|
118 elif isinstance(req.args[arg], basestring):
|
|
119 hdf['args.%s' % arg] = req.args[arg]
|
|
120 # others are file uploads
|
|
121
|
|
122
|
|
123 class RequestDispatcher(Component):
|
|
124 """Component responsible for dispatching requests to registered handlers."""
|
|
125
|
|
126 authenticators = ExtensionPoint(IAuthenticator)
|
|
127 handlers = ExtensionPoint(IRequestHandler)
|
|
128
|
|
129 filters = OrderedExtensionsOption('trac', 'request_filters', IRequestFilter,
|
|
130 doc="""Ordered list of filters to apply to all requests
|
|
131 (''since 0.10'').""")
|
|
132
|
|
133 default_handler = ExtensionOption('trac', 'default_handler',
|
|
134 IRequestHandler, 'WikiModule',
|
|
135 """Name of the component that handles requests to the base URL.
|
|
136
|
|
137 Options include `TimeLineModule`, `RoadmapModule`, `BrowserModule`,
|
|
138 `QueryModule`, `ReportModule` and `NewticketModule` (''since 0.9'').""")
|
|
139
|
|
140 # Public API
|
|
141
|
|
142 def authenticate(self, req):
|
|
143 for authenticator in self.authenticators:
|
|
144 authname = authenticator.authenticate(req)
|
|
145 if authname:
|
|
146 return authname
|
|
147 else:
|
|
148 return 'anonymous'
|
|
149
|
|
150 def dispatch(self, req):
|
|
151 """Find a registered handler that matches the request and let it process
|
|
152 it.
|
|
153
|
|
154 In addition, this method initializes the HDF data set and adds the web
|
|
155 site chrome.
|
|
156 """
|
|
157 # FIXME: For backwards compatibility, should be removed in 0.11
|
|
158 self.env.href = req.href
|
|
159 self.env.abs_href = req.abs_href
|
|
160
|
|
161 # Select the component that should handle the request
|
|
162 chosen_handler = None
|
|
163 if not req.path_info or req.path_info == '/':
|
|
164 chosen_handler = self.default_handler
|
|
165 else:
|
|
166 for handler in self.handlers:
|
|
167 if handler.match_request(req):
|
|
168 chosen_handler = handler
|
|
169 break
|
|
170
|
|
171 for filter_ in self.filters:
|
|
172 chosen_handler = filter_.pre_process_request(req, chosen_handler)
|
|
173
|
|
174 if not chosen_handler:
|
|
175 raise HTTPNotFound('No handler matched request to %s',
|
|
176 req.path_info)
|
|
177
|
|
178 # Attach user information to the request
|
|
179 anonymous_request = getattr(chosen_handler, 'anonymous_request', False)
|
|
180 if anonymous_request:
|
|
181 req.authname = 'anonymous'
|
|
182 req.perm = NoPermissionCache()
|
|
183 else:
|
|
184 req.authname = self.authenticate(req)
|
|
185 req.perm = PermissionCache(self.env, req.authname)
|
|
186 req.session = Session(self.env, req)
|
|
187
|
|
188 # Prepare HDF for the clearsilver template
|
|
189 use_template = getattr(chosen_handler, 'use_template', True)
|
|
190 if use_template:
|
|
191 chrome = Chrome(self.env)
|
|
192 req.hdf = HDFWrapper(loadpaths=chrome.get_all_templates_dirs())
|
|
193 populate_hdf(req.hdf, self.env, req)
|
|
194 chrome.populate_hdf(req, chosen_handler)
|
|
195
|
|
196 # Process the request and render the template
|
|
197 try:
|
|
198 try:
|
|
199 resp = chosen_handler.process_request(req)
|
|
200 if resp:
|
|
201 for filter_ in reversed(self.filters):
|
|
202 resp = filter_.post_process_request(req, *resp)
|
|
203 template, content_type = resp
|
|
204 req.display(template, content_type or 'text/html')
|
|
205 else:
|
|
206 for filter_ in reversed(self.filters):
|
|
207 filter_.post_process_request(req, None, None)
|
|
208 except PermissionError, e:
|
|
209 raise HTTPForbidden(to_unicode(e))
|
|
210 except TracError, e:
|
|
211 raise HTTPInternalError(e.message)
|
|
212 finally:
|
|
213 # Give the session a chance to persist changes
|
|
214 if req.session:
|
|
215 req.session.save()
|
|
216
|
|
217
|
|
218 def dispatch_request(environ, start_response):
|
|
219 """Main entry point for the Trac web interface.
|
|
220
|
|
221 @param environ: the WSGI environment dict
|
|
222 @param start_response: the WSGI callback for starting the response
|
|
223 """
|
|
224 if 'mod_python.options' in environ:
|
|
225 options = environ['mod_python.options']
|
|
226 environ.setdefault('trac.env_path', options.get('TracEnv'))
|
|
227 environ.setdefault('trac.env_parent_dir',
|
|
228 options.get('TracEnvParentDir'))
|
|
229 environ.setdefault('trac.env_index_template',
|
|
230 options.get('TracEnvIndexTemplate'))
|
|
231 environ.setdefault('trac.template_vars',
|
|
232 options.get('TracTemplateVars'))
|
|
233 environ.setdefault('trac.locale', options.get('TracLocale'))
|
|
234
|
|
235 if 'TracUriRoot' in options:
|
|
236 # Special handling of SCRIPT_NAME/PATH_INFO for mod_python, which
|
|
237 # tends to get confused for whatever reason
|
|
238 root_uri = options['TracUriRoot'].rstrip('/')
|
|
239 request_uri = environ['REQUEST_URI'].split('?', 1)[0]
|
|
240 if not request_uri.startswith(root_uri):
|
|
241 raise ValueError('TracUriRoot set to %s but request URL '
|
|
242 'is %s' % (root_uri, request_uri))
|
|
243 environ['SCRIPT_NAME'] = root_uri
|
|
244 environ['PATH_INFO'] = urllib.unquote(request_uri[len(root_uri):])
|
|
245
|
|
246 else:
|
|
247 environ.setdefault('trac.env_path', os.getenv('TRAC_ENV'))
|
|
248 environ.setdefault('trac.env_parent_dir',
|
|
249 os.getenv('TRAC_ENV_PARENT_DIR'))
|
|
250 environ.setdefault('trac.env_index_template',
|
|
251 os.getenv('TRAC_ENV_INDEX_TEMPLATE'))
|
|
252 environ.setdefault('trac.template_vars',
|
|
253 os.getenv('TRAC_TEMPLATE_VARS'))
|
|
254 environ.setdefault('trac.locale', '')
|
|
255
|
|
256 locale.setlocale(locale.LC_ALL, environ['trac.locale'])
|
|
257
|
|
258 # Allow specifying the python eggs cache directory using SetEnv
|
|
259 if 'mod_python.subprocess_env' in environ:
|
|
260 egg_cache = environ['mod_python.subprocess_env'].get('PYTHON_EGG_CACHE')
|
|
261 if egg_cache:
|
|
262 os.environ['PYTHON_EGG_CACHE'] = egg_cache
|
|
263
|
|
264 # Determine the environment
|
|
265 env_path = environ.get('trac.env_path')
|
|
266 if not env_path:
|
|
267 env_parent_dir = environ.get('trac.env_parent_dir')
|
|
268 env_paths = environ.get('trac.env_paths')
|
|
269 if env_parent_dir or env_paths:
|
|
270 # The first component of the path is the base name of the
|
|
271 # environment
|
|
272 path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
|
|
273 env_name = path_info.pop(0)
|
|
274
|
|
275 if not env_name:
|
|
276 # No specific environment requested, so render an environment
|
|
277 # index page
|
|
278 send_project_index(environ, start_response, env_parent_dir,
|
|
279 env_paths)
|
|
280 return []
|
|
281
|
|
282 # To make the matching patterns of request handlers work, we append
|
|
283 # the environment name to the `SCRIPT_NAME` variable, and keep only
|
|
284 # the remaining path in the `PATH_INFO` variable.
|
|
285 environ['SCRIPT_NAME'] = Href(environ['SCRIPT_NAME'])(env_name)
|
|
286 environ['PATH_INFO'] = '/'.join([''] + path_info)
|
|
287
|
|
288 if env_parent_dir:
|
|
289 env_path = os.path.join(env_parent_dir, env_name)
|
|
290 else:
|
|
291 env_path = get_environments(environ).get(env_name)
|
|
292
|
|
293 if not env_path or not os.path.isdir(env_path):
|
|
294 start_response('404 Not Found', [])
|
|
295 return ['Environment not found']
|
|
296
|
|
297 if not env_path:
|
|
298 raise EnvironmentError('The environment options "TRAC_ENV" or '
|
|
299 '"TRAC_ENV_PARENT_DIR" or the mod_python '
|
|
300 'options "TracEnv" or "TracEnvParentDir" are '
|
|
301 'missing. Trac requires one of these options '
|
|
302 'to locate the Trac environment(s).')
|
|
303 env = _open_environment(env_path, run_once=environ['wsgi.run_once'])
|
|
304
|
|
305 if env.base_url:
|
|
306 environ['trac.base_url'] = env.base_url
|
|
307
|
|
308 req = Request(environ, start_response)
|
|
309 try:
|
|
310 db = env.get_db_cnx()
|
|
311 try:
|
|
312 try:
|
|
313 dispatcher = RequestDispatcher(env)
|
|
314 dispatcher.dispatch(req)
|
|
315 except RequestDone:
|
|
316 pass
|
|
317 return req._response or []
|
|
318 finally:
|
|
319 db.close()
|
|
320
|
|
321 except HTTPException, e:
|
|
322 env.log.warn(e)
|
|
323 if req.hdf:
|
|
324 req.hdf['title'] = e.reason or 'Error'
|
|
325 req.hdf['error'] = {
|
|
326 'title': e.reason or 'Error',
|
|
327 'type': 'TracError',
|
|
328 'message': e.message
|
|
329 }
|
|
330 try:
|
|
331 req.send_error(sys.exc_info(), status=e.code)
|
|
332 except RequestDone:
|
|
333 return []
|
|
334
|
|
335 except Exception, e:
|
|
336 env.log.exception(e)
|
|
337
|
|
338 if req.hdf:
|
|
339 req.hdf['title'] = to_unicode(e) or 'Error'
|
|
340 req.hdf['error'] = {
|
|
341 'title': to_unicode(e) or 'Error',
|
|
342 'type': 'internal',
|
|
343 'traceback': get_last_traceback()
|
|
344 }
|
|
345 try:
|
|
346 req.send_error(sys.exc_info(), status=500)
|
|
347 except RequestDone:
|
|
348 return []
|
|
349
|
|
350 def send_project_index(environ, start_response, parent_dir=None,
|
|
351 env_paths=None):
|
|
352 from trac.config import default_dir
|
|
353
|
|
354 req = Request(environ, start_response)
|
|
355
|
|
356 loadpaths = [default_dir('templates')]
|
|
357 if req.environ.get('trac.env_index_template'):
|
|
358 tmpl_path, template = os.path.split(req.environ['trac.env_index_template'])
|
|
359 loadpaths.insert(0, tmpl_path)
|
|
360 else:
|
|
361 template = 'index.cs'
|
|
362 req.hdf = HDFWrapper(loadpaths)
|
|
363
|
|
364 tmpl_vars = {}
|
|
365 if req.environ.get('trac.template_vars'):
|
|
366 for pair in req.environ['trac.template_vars'].split(','):
|
|
367 key, val = pair.split('=')
|
|
368 req.hdf[key] = val
|
|
369
|
|
370 if parent_dir and not env_paths:
|
|
371 env_paths = dict([(filename, os.path.join(parent_dir, filename))
|
|
372 for filename in os.listdir(parent_dir)])
|
|
373
|
|
374 try:
|
|
375 href = Href(req.base_path)
|
|
376 projects = []
|
|
377 for env_name, env_path in get_environments(environ).items():
|
|
378 try:
|
|
379 env = _open_environment(env_path,
|
|
380 run_once=environ['wsgi.run_once'])
|
|
381 proj = {
|
|
382 'name': env.project_name,
|
|
383 'description': env.project_description,
|
|
384 'href': href(env_name)
|
|
385 }
|
|
386 except Exception, e:
|
|
387 proj = {'name': env_name, 'description': to_unicode(e)}
|
|
388 projects.append(proj)
|
|
389 projects.sort(lambda x, y: cmp(x['name'].lower(), y['name'].lower()))
|
|
390
|
|
391 req.hdf['projects'] = projects
|
|
392 req.display(template)
|
|
393 except RequestDone:
|
|
394 pass
|
|
395
|
|
396 def get_environments(environ, warn=False):
|
|
397 """Retrieve canonical environment name to path mapping.
|
|
398
|
|
399 The environments may not be all valid environments, but they are good
|
|
400 candidates.
|
|
401 """
|
|
402 env_paths = environ.get('trac.env_paths', [])
|
|
403 env_parent_dir = environ.get('trac.env_parent_dir')
|
|
404 if env_parent_dir:
|
|
405 env_parent_dir = os.path.normpath(env_parent_dir)
|
|
406 paths = dircache.listdir(env_parent_dir)[:]
|
|
407 dircache.annotate(env_parent_dir, paths)
|
|
408 env_paths += [os.path.join(env_parent_dir, project) \
|
|
409 for project in paths if project[-1] == '/']
|
|
410 envs = {}
|
|
411 for env_path in env_paths:
|
|
412 env_path = os.path.normpath(env_path)
|
|
413 if not os.path.isdir(env_path):
|
|
414 continue
|
|
415 env_name = os.path.split(env_path)[1]
|
|
416 if env_name in envs:
|
|
417 if warn:
|
|
418 print >> sys.stderr, ('Warning: Ignoring project "%s" since '
|
|
419 'it conflicts with project "%s"'
|
|
420 % (env_path, envs[env_name]))
|
|
421 else:
|
|
422 envs[env_name] = env_path
|
|
423 return envs
|