Package trac ::
Module env
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2003-2009 Edgewall Software
4 # Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
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. The terms
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
10 #
11 # This software consists of voluntary contributions made by many
12 # individuals. For the exact contribution history, see the revision
13 # history and logs, available at http://trac.edgewall.org/log/.
14 #
15 # Author: Jonas Borgström <jonas@edgewall.com>
16
17 import os.path
18 import setuptools
19 import sys
20 from urlparse import urlsplit
21
22 from trac import db_default
23 from trac .admin import AdminCommandError , IAdminCommandProvider
24 from trac .cache import CacheManager
25 from trac .config import *
26 from trac .core import Component , ComponentManager, implements , Interface , \
27 ExtensionPoint , TracError
28 from trac .db .api import DatabaseManager , get_read_db , with_transaction
29 from trac .util import copytree , create_file , get_pkginfo , makedirs
30 from trac .util .compat import any, sha1
31 from trac .util .concurrency import threading
32 from trac .util .text import exception_to_unicode , path_to_unicode , printerr , \
33 printout
34 from trac .util .translation import _ , N_
35 from trac .versioncontrol import RepositoryManager
36 from trac .web .href import Href
37
38 __all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
42 """Provider of system information, displayed in the "About Trac" page and
43 in internal error reports.
44 """
46 """Yield a sequence of `(name, version)` tuples describing the name and
47 version information of external packages used by a component.
48 """
49
52 """Extension point interface for components that need to participate in the
53 creation and upgrading of Trac environments, for example to create
54 additional database tables."""
55
57 """Called when a new Trac environment is created."""
58
60 """Called when Trac checks whether the environment needs to be upgraded.
61
62 Should return `True` if this participant needs an upgrade to be
63 performed, `False` otherwise.
64 """
65
67 """Actually perform an environment upgrade.
68
69 Implementations of this method don't need to commit any database
70 transactions. This is done implicitly for each participant
71 if the upgrade succeeds without an error being raised.
72
73 However, if the `upgrade_environment` consists of small, restartable,
74 steps of upgrade, it can decide to commit on its own after each
75 successful step.
76 """
77
80 """Trac environment manager.
81
82 Trac stores project information in a Trac environment. It consists of a
83 directory structure containing among other things:
84 * a configuration file
85 * an SQLite database (stores tickets, wiki pages...)
86 * project-specific templates and plugins
87 * wiki and ticket attachments
88 """
89 implements (ISystemInfoProvider)
90
91 required = True
92
93 system_info_providers = ExtensionPoint (ISystemInfoProvider)
94 setup_participants = ExtensionPoint (IEnvironmentSetupParticipant )
95
96 shared_plugins_dir = PathOption ('inherit', 'plugins_dir', '',
97 """Path to the //shared plugins directory//.
98
99 Plugins in that directory are loaded in addition to those in the
100 directory of the environment `plugins`, with this one taking
101 precedence.
102
103 (''since 0.11'')""")
104
105 base_url = Option ('trac', 'base_url', '',
106 """Reference URL for the Trac deployment.
107
108 This is the base URL that will be used when producing documents that
109 will be used outside of the web browsing context, like for example
110 when inserting URLs pointing to Trac resources in notification
111 e-mails.""")
112
113 base_url_for_redirect = BoolOption ('trac', 'use_base_url_for_redirect',
114 False,
115 """Optionally use `[trac] base_url` for redirects.
116
117 In some configurations, usually involving running Trac behind a HTTP
118 proxy, Trac can't automatically reconstruct the URL that is used to
119 access it. You may need to use this option to force Trac to use the
120 `base_url` setting also for redirects. This introduces the obvious
121 limitation that this environment will only be usable when accessible
122 from that URL, as redirects are frequently used. ''(since 0.10.5)''""")
123
124 secure_cookies = BoolOption ('trac', 'secure_cookies', False,
125 """Restrict cookies to HTTPS connections.
126
127 When true, set the `secure` flag on all cookies so that they are
128 only sent to the server on HTTPS connections. Use this if your Trac
129 instance is only accessible through HTTPS. (''since 0.11.2'')""")
130
131 project_name = Option ('project', 'name', 'My Project',
132 """Name of the project.""")
133
134 project_description = Option ('project', 'descr', 'My example project',
135 """Short description of the project.""")
136
137 project_url = Option ('project', 'url', '',
138 """URL of the main project web site, usually the website in which
139 the `base_url` resides. This is used in notification e-mails.""")
140
141 project_admin = Option ('project', 'admin', '',
142 """E-Mail address of the project's administrator.""")
143
144 project_admin_trac_url = Option ('project', 'admin_trac_url', '.',
145 """Base URL of a Trac instance where errors in this Trac should be
146 reported.
147
148 This can be an absolute or relative URL, or '.' to reference this
149 Trac instance. An empty value will disable the reporting buttons.
150 (''since 0.11.3'')""")
151
152 project_footer = Option ('project', 'footer',
153 N_('Visit the Trac open source project at<br />'
154 '<a href="http://trac.edgewall.org/">'
155 'http://trac.edgewall.org/</a>'),
156 """Page footer text (right-aligned).""")
157
158 project_icon = Option ('project', 'icon', 'common/trac.ico',
159 """URL of the icon of the project.""")
160
161 log_type = Option ('logging', 'log_type', 'none',
162 """Logging facility to use.
163
164 Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
165
166 log_file = Option ('logging', 'log_file', 'trac.log',
167 """If `log_type` is `file`, this should be a path to the log-file.
168 Relative paths are resolved relative to the `log` directory of the
169 environment.""")
170
171 log_level = Option ('logging', 'log_level', 'DEBUG',
172 """Level of verbosity in log.
173
174 Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
175
176 log_format = Option ('logging', 'log_format', None,
177 """Custom logging format.
178
179 If nothing is set, the following will be used:
180
181 Trac[$(module)s] $(levelname)s: $(message)s
182
183 In addition to regular key names supported by the Python logger library
184 (see http://docs.python.org/library/logging.html), one could use:
185 - $(path)s the path for the current environment
186 - $(basename)s the last path component of the current environment
187 - $(project)s the project name
188
189 Note the usage of `$(...)s` instead of `%(...)s` as the latter form
190 would be interpreted by the ConfigParser itself.
191
192 Example:
193 `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
194
195 ''(since 0.10.5)''""")
196
197 - def __init__ (self, path, create=False, options=[]):
198 """Initialize the Trac environment.
199
200 @param path: the absolute path to the Trac environment
201 @param create: if `True`, the environment is created and populated with
202 default data; otherwise, the environment is expected to
203 already exist.
204 @param options: A list of `(section, name, value)` tuples that define
205 configuration options
206 """
207 ComponentManager.__init__ (self)
208
209 self.path = path
210 self.systeminfo = []
211 self._href = self._abs_href = None
212
213 if create :
214 self.create (options )
215 else:
216 self.verify ()
217 self.setup_config ()
218
219 if create :
220 for setup_participant in self.setup_participants :
221 setup_participant.environment_created ()
222
224 """Return a list of `(name, version)` tuples describing the name and
225 version information of external packages used by Trac and plugins.
226 """
227 info = self.systeminfo[:]
228 for provider in self.system_info_providers :
229 info.extend(provider.get_system_info () or [])
230 info.sort(key=lambda (name , version ): (name != 'Trac', name .lower()))
231 return info
232
233 # ISystemInfoProvider methods
234
236 from trac import core , __version__ as VERSION
237 yield 'Trac', get_pkginfo (core ).get ('version', VERSION)
238 yield 'Python', sys.version
239 yield 'setuptools', setuptools.__version__
240 from trac .util .datefmt import pytz
241 if pytz is not None:
242 yield 'pytz', pytz.__version__
243
245 """Initialize additional member variables for components.
246
247 Every component activated through the `Environment` object gets three
248 member variables: `env` (the environment object), `config` (the
249 environment configuration) and `log` (a logger object)."""
250 component.env = self
251 component.config = self.config
252 component.log = self.log
253
255 name = name_or_class
256 if not isinstance(name_or_class, basestring):
257 name = name_or_class.__module__ + '.' + name_or_class.__name__
258 return name .lower()
259
260 @property
262 try:
263 return self._rules
264 except AttributeError:
265 self._rules = {}
266 for name , value in self.config .options ('components'):
267 if name .endswith('.*'):
268 name = name [:-2]
269 self._rules[name .lower()] = value.lower() in ('enabled', 'on')
270 return self._rules
271
273 """Implemented to only allow activation of components that are not
274 disabled in the configuration.
275
276 This is called by the `ComponentManager` base class when a component is
277 about to be activated. If this method returns `False`, the component
278 does not get activated. If it returns `None`, the component only gets
279 activated if it is located in the `plugins` directory of the
280 enironment.
281 """
282 component_name = self._component_name(cls)
283
284 # Disable the pre-0.11 WebAdmin plugin
285 # Please note that there's no recommendation to uninstall the
286 # plugin because doing so would obviously break the backwards
287 # compatibility that the new integration administration
288 # interface tries to provide for old WebAdmin extensions
289 if component_name.startswith('webadmin.'):
290 self.log .info('The legacy TracWebAdmin plugin has been '
291 'automatically disabled, and the integrated '
292 'administration interface will be used '
293 'instead.')
294 return False
295
296 rules = self._component_rules
297 cname = component_name
298 while cname:
299 enabled = rules .get (cname)
300 if enabled is not None:
301 return enabled
302 idx = cname.rfind('.')
303 if idx < 0:
304 break
305 cname = cname[:idx]
306
307 # By default, all components in the trac package except
308 # trac.test are enabled
309 return component_name.startswith('trac.') and \
310 not component_name.startswith('trac.test.') or None
311
313 """Enable a component or module."""
314 self._component_rules[self._component_name(cls)] = True
315
317 """Verify that the provided path points to a valid Trac environment
318 directory."""
319 fd = open (os.path .join (self.path , 'VERSION'), 'r')
320 try:
321 assert fd.read (26) == 'Trac Environment Version 1'
322 finally:
323 fd.close ()
324
326 """Return a database connection from the connection pool (deprecated)
327
328 Use `with_transaction` for obtaining a writable database connection
329 and `get_read_db` for anything else.
330 """
331 return get_read_db (self)
332
334 """Decorator for transaction functions.
335
336 See `trac.db.api.with_transaction` for detailed documentation."""
337 return with_transaction (self, db )
338
340 """Return a database connection for read purposes.
341
342 See `trac.db.api.get_read_db` for detailed documentation."""
343 return get_read_db (self)
344
354
356 """Return the version control repository with the given name, or the
357 default repository if `None`.
358
359 The standard way of retrieving repositories is to use the methods
360 of `RepositoryManager`. This method is retained here for backward
361 compatibility.
362
363 @param reponame: the name of the repository
364 @param authname: the user name for authorization (not used anymore,
365 left here for compatibility with 0.11)
366 """
367 return RepositoryManager (self).get_repository (reponame)
368
369 - def create (self, options=[]):
370 """Create the basic directory structure of the environment, initialize
371 the database and populate the configuration file with default values.
372
373 If options contains ('inherit', 'file'), default values will not be
374 loaded; they are expected to be provided by that file or other options.
375 """
376 # Create the directory structure
377 if not os.path .exists (self.path ):
378 os.mkdir(self.path )
379 os.mkdir(self.get_log_dir ())
380 os.mkdir(self.get_htdocs_dir ())
381 os.mkdir(os.path .join (self.path , 'plugins'))
382
383 # Create a few files
384 create_file (os.path .join (self.path , 'VERSION'),
385 'Trac Environment Version 1\n')
386 create_file (os.path .join (self.path , 'README'),
387 'This directory contains a Trac environment.\n'
388 'Visit http://trac.edgewall.org/ for more information.\n')
389
390 # Setup the default configuration
391 os.mkdir(os.path .join (self.path , 'conf'))
392 create_file (os.path .join (self.path , 'conf', 'trac.ini.sample'))
393 config = Configuration (os.path .join (self.path , 'conf', 'trac.ini'))
394 for section, name , value in options :
395 config .set (section, name , value)
396 config .save ()
397 self.setup_config ()
398 if not any((section, option) == ('inherit', 'file')
399 for section, option, value in options ):
400 self.config .set_defaults (self)
401 self.config .save ()
402
403 # Create the database
404 DatabaseManager (self).init_db ()
405
407 """Return the current version of the database.
408 If the optional argument `initial` is set to `True`, the version
409 of the database used at the time of creation will be returned.
410
411 In practice, for database created before 0.11, this will return `False`
412 which is "older" than any db version number.
413
414 :since 0.11:
415 """
416 if not db :
417 db = self.get_db_cnx ()
418 cursor = db .cursor ()
419 cursor .execute ("SELECT value FROM system "
420 "WHERE name='%sdatabase_version'" %
421 (initial and 'initial_' or ''))
422 row = cursor .fetchone ()
423 return row and int(row[0])
424
433
435 """Return absolute path to the templates directory."""
436 return os.path .join (self.path , 'templates')
437
439 """Return absolute path to the htdocs directory."""
440 return os.path .join (self.path , 'htdocs')
441
443 """Return absolute path to the log directory."""
444 return os.path .join (self.path , 'log')
445
447 """Initialize the logging sub-system."""
448 from trac .log import logger_handler_factory
449 logtype = self.log_type
450 logfile = self.log_file
451 if logtype == 'file' and not os.path .isabs(logfile ):
452 logfile = os.path .join (self.get_log_dir (), logfile )
453 format = self.log_format
454 logid = 'Trac.%s' % sha1(self.path ).hexdigest()
455 if format :
456 format = format .replace ('$(', '%(') \
457 .replace ('%(path)s', self.path ) \
458 .replace ('%(basename)s', os.path .basename(self.path )) \
459 .replace ('%(project)s', self.project_name )
460 self.log , self._log_handler = logger_handler_factory (
461 logtype, logfile , self.log_level , logid, format =format )
462 from trac import core , __version__ as VERSION
463 self.log .info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
464 get_pkginfo (core ).get ('version', VERSION))
465
467 """Generator that yields information about all known users, i.e. users
468 that have logged in to this Trac environment and possibly set their name
469 and email.
470
471 This function generates one tuple for every user, of the form
472 (username, name, email) ordered alpha-numerically by username.
473
474 @param cnx: the database connection; if ommitted, a new connection is
475 retrieved
476 """
477 if not cnx :
478 cnx = self.get_db_cnx ()
479 cursor = cnx .cursor ()
480 cursor .execute ("SELECT DISTINCT s.sid, n.value, e.value "
481 "FROM session AS s "
482 " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
483 " and n.authenticated=1 AND n.name = 'name') "
484 " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
485 " AND e.authenticated=1 AND e.name = 'email') "
486 "WHERE s.authenticated=1 ORDER BY s.sid")
487 for username , name , email in cursor :
488 yield username , name , email
489
490 - def backup (self, dest=None):
491 """Create a backup of the database.
492
493 @param dest: Destination file; if not specified, the backup is stored in
494 a file called db_name.trac_version.bak
495 """
496 return DatabaseManager (self).backup (dest)
497
499 """Return whether the environment needs to be upgraded."""
500 db = self.get_db_cnx ()
501 for participant in self.setup_participants :
502 if participant.environment_needs_upgrade (db ):
503 self.log .warning('Component %s requires environment upgrade',
504 participant)
505 return True
506 return False
507
508 - def upgrade (self, backup=False, backup_dest=None):
509 """Upgrade database.
510
511 @param backup: whether or not to backup before upgrading
512 @param backup_dest: name of the backup file
513 @return: whether the upgrade was performed
514 """
515 upgraders = []
516 db = self.get_read_db ()
517 for participant in self.setup_participants :
518 if participant.environment_needs_upgrade (db ):
519 upgraders.append(participant)
520 if not upgraders:
521 return
522
523 if backup :
524 self.backup (backup_dest)
525
526 for participant in upgraders:
527 self.log .info("%s.%s upgrading...", participant.__module__,
528 participant.__class__.__name__)
529 with_transaction (self)(participant.upgrade_environment )
530 # Database schema may have changed, so close all connections
531 DatabaseManager (self).shutdown ()
532 return True
533
534 @property
536 """The application root path"""
537 if not self._href:
538 self._href = Href (urlsplit(self.abs_href .base)[2])
539 return self._href
540
541 @property
543 """The application URL"""
544 if not self._abs_href:
545 if not self.base_url :
546 self.log .warn('base_url option not set in configuration, '
547 'generated links may be incorrect')
548 self._abs_href = Href ('')
549 else:
550 self._abs_href = Href (self.base_url )
551 return self._abs_href
552
574
584
586 """Each db version should have its own upgrade module, named
587 upgrades/dbN.py, where 'N' is the version number (int).
588 """
589 cursor = db .cursor ()
590 dbver = self.env .get_version ()
591 for i in range(dbver + 1, db_default .db_version + 1):
592 name = 'db%i' % i
593 try:
594 upgrades = __import__('upgrades', globals(), locals(), [name ])
595 script = getattr(upgrades , name )
596 except AttributeError:
597 raise TracError (_ ('No upgrade module for version %(num)i '
598 '(%(version)s.py)', num=i , version =name ))
599 script.do_upgrade (self.env , i , cursor )
600 cursor .execute ("""
601 UPDATE system SET value=%s WHERE name='database_version'
602 """, (i ,))
603 self.log .info('Upgraded database version from %d to %d', i - 1, i )
604 db .commit ()
605 self._update_sample_config()
606
607 # Internal methods
608
610 filename = os.path .join (self.env .path , 'conf', 'trac.ini.sample')
611 if not os.path .isfile (filename ):
612 return
613 config = Configuration (filename )
614 for section, default_options in config .defaults ().iteritems():
615 for name , value in default_options.iteritems():
616 config .set (section, name , value)
617 try:
618 config .save ()
619 self.log .info('Wrote sample configuration file with the new '
620 'settings and their default values: %s',
621 filename )
622 except IOError, e :
623 self.log .warn('Couldn\'t write sample configuration file (%s)', e ,
624 exc_info=True)