trac-ticketlinks / trac / env.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# -*- coding: utf-8 -*-
#
# Copyright (C) 2003-2008 Edgewall Software
# Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Jonas Borgström <jonas@edgewall.com>

import os
try:
    import threading
except ImportError:
    import dummy_threading as threading
import setuptools
import sys
from urlparse import urlsplit

from trac import db_default
from trac.config import *
from trac.core import Component, ComponentManager, implements, Interface, \
                      ExtensionPoint, TracError
from trac.db import DatabaseManager
from trac.util import get_pkginfo
from trac.util.translation import _
from trac.versioncontrol import RepositoryManager
from trac.web.href import Href

__all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']


class IEnvironmentSetupParticipant(Interface):
    """Extension point interface for components that need to participate in the
    creation and upgrading of Trac environments, for example to create
    additional database tables."""

    def environment_created():
        """Called when a new Trac environment is created."""

    def environment_needs_upgrade(db):
        """Called when Trac checks whether the environment needs to be upgraded.
        
        Should return `True` if this participant needs an upgrade to be
        performed, `False` otherwise.
        """

    def upgrade_environment(db):
        """Actually perform an environment upgrade.
        
        Implementations of this method should not commit any database
        transactions. This is done implicitly after all participants have
        performed the upgrades they need without an error being raised.
        """


class Environment(Component, ComponentManager):
    """Trac stores project information in a Trac environment.

    A Trac environment consists of a directory structure containing among other
    things:
     * a configuration file.
     * an SQLite database (stores tickets, wiki pages...)
     * Project specific templates and plugins.
     * wiki and ticket attachments.
    """   
    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)

    shared_plugins_dir = PathOption('inherit', 'plugins_dir', '',
        """Path of the directory containing additional plugins.
        
        Plugins in that directory are loaded in addition to those in the
        directory of the environment `plugins`, with this one taking 
        precedence.
        
        (''since 0.11'')""")

    base_url = Option('trac', 'base_url', '',
        """Reference URL for the Trac deployment.
        
        This is the base URL that will be used when producing documents that
        will be used outside of the web browsing context, like for example
        when inserting URLs pointing to Trac resources in notification
        e-mails.""")

    base_url_for_redirect = BoolOption('trac', 'use_base_url_for_redirect',
            False, 
        """Optionally use `[trac] base_url` for redirects.
        
        In some configurations, usually involving running Trac behind a HTTP
        proxy, Trac can't automatically reconstruct the URL that is used to
        access it. You may need to use this option to force Trac to use the
        `base_url` setting also for redirects. This introduces the obvious
        limitation that this environment will only be usable when accessible
        from that URL, as redirects are frequently used. ''(since 0.10.5)''""")

    secure_cookies = BoolOption('trac', 'secure_cookies', False,
        """Restrict cookies to HTTPS connections.
        
        When true, set the `secure` flag on all cookies so that they are
        only sent to the server on HTTPS connections. Use this if your Trac
        instance is only accessible through HTTPS. (''since 0.11.2'')""")

    project_name = Option('project', 'name', 'My Project',
        """Name of the project.""")

    project_description = Option('project', 'descr', 'My example project',
        """Short description of the project.""")

    project_url = Option('project', 'url', '',
        """URL of the main project web site, usually the website in which
        the `base_url` resides.""")

    project_admin = Option('project', 'admin', '',
        """E-Mail address of the project's administrator.""")

    project_footer = Option('project', 'footer',
                            'Visit the Trac open source project at<br />'
                            '<a href="http://trac.edgewall.org/">'
                            'http://trac.edgewall.org/</a>',
        """Page footer text (right-aligned).""")

    project_icon = Option('project', 'icon', 'common/trac.ico',
        """URL of the icon of the project.""")

    log_type = Option('logging', 'log_type', 'none',
        """Logging facility to use.
        
        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")

    log_file = Option('logging', 'log_file', 'trac.log',
        """If `log_type` is `file`, this should be a path to the log-file.""")

    log_level = Option('logging', 'log_level', 'DEBUG',
        """Level of verbosity in log.
        
        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")

    log_format = Option('logging', 'log_format', None,
        """Custom logging format.

        If nothing is set, the following will be used:
        
        Trac[$(module)s] $(levelname)s: $(message)s

        In addition to regular key names supported by the Python logger library
        library (see http://docs.python.org/lib/node422.html), one could use:
         - $(path)s     the path for the current environment
         - $(basename)s the last path component of the current environment
         - $(project)s  the project name

         Note the usage of `$(...)s` instead of `%(...)s` as the latter form
         would be interpreted by the ConfigParser itself.

         Example:
         ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s

         (since 0.10.5)""")

    def __init__(self, path, create=False, options=[]):
        """Initialize the Trac environment.
        
        @param path:   the absolute path to the Trac environment
        @param create: if `True`, the environment is created and populated with
                       default data; otherwise, the environment is expected to
                       already exist.
        @param options: A list of `(section, name, value)` tuples that define
                        configuration options
        """
        ComponentManager.__init__(self)

        self.path = path
        self.setup_config(load_defaults=create)
        self.setup_log()

        from trac import core, __version__ as VERSION
        self.systeminfo = [
            ('Trac', get_pkginfo(core).get('version', VERSION)),
            ('Python', sys.version),
            ('setuptools', setuptools.__version__),
            ]
        self._href = self._abs_href = None

        from trac.loader import load_components
        plugins_dir = self.config.get('inherit', 'plugins_dir')
        load_components(self, plugins_dir and (plugins_dir,))

        if create:
            self.create(options)
        else:
            self.verify()

        if create:
            for setup_participant in self.setup_participants:
                setup_participant.environment_created()

    def component_activated(self, component):
        """Initialize additional member variables for components.
        
        Every component activated through the `Environment` object gets three
        member variables: `env` (the environment object), `config` (the
        environment configuration) and `log` (a logger object)."""
        component.env = self
        component.config = self.config
        component.log = self.log

    def is_component_enabled(self, cls):
        """Implemented to only allow activation of components that are not
        disabled in the configuration.
        
        This is called by the `ComponentManager` base class when a component is
        about to be activated. If this method returns false, the component does
        not get activated."""
        if not isinstance(cls, basestring):
            component_name = (cls.__module__ + '.' + cls.__name__).lower()
        else:
            component_name = cls.lower()

        rules = [(name.lower(), value.lower() in ('enabled', 'on'))
                 for name, value in self.config.options('components')]
        rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))

        for pattern, enabled in rules:
            if component_name == pattern or pattern.endswith('*') \
                    and component_name.startswith(pattern[:-1]):
                # Disable the pre-0.11 WebAdmin plugin
                # Please note that there's no recommendation to uninstall the
                # plugin because doing so would obviously break the backwards
                # compatibility that the new integration administration
                # interface tries to provide for old WebAdmin extensions
                if component_name.startswith('webadmin.'):
                    self.log.info('The legacy TracWebAdmin plugin has been '
                                  'automatically disabled, and the integrated '
                                  'administration interface will be used '
                                  'instead.')
                    return False
                return enabled

        # versioncontrol components are enabled if the repository is configured
        # FIXME: this shouldn't be hardcoded like this
        if component_name.startswith('trac.versioncontrol.'):
            return self.config.get('trac', 'repository_dir') != ''

        # By default, all components in the trac package are enabled
        return component_name.startswith('trac.')

    def verify(self):
        """Verify that the provided path points to a valid Trac environment
        directory."""
        fd = open(os.path.join(self.path, 'VERSION'), 'r')
        try:
            assert fd.read(26) == 'Trac Environment Version 1'
        finally:
            fd.close()

    def get_db_cnx(self):
        """Return a database connection from the connection pool."""
        return DatabaseManager(self).get_connection()

    def shutdown(self, tid=None):
        """Close the environment."""
        RepositoryManager(self).shutdown(tid)
        DatabaseManager(self).shutdown(tid)

    def get_repository(self, authname=None):
        """Return the version control repository configured for this
        environment.
        
        @param authname: user name for authorization
        """
        return RepositoryManager(self).get_repository(authname)

    def create(self, options=[]):
        """Create the basic directory structure of the environment, initialize
        the database and populate the configuration file with default values.

        If options contains ('inherit', 'file'), default values will not be
        loaded; they are expected to be provided by that file or other options.
        """
        def _create_file(fname, data=None):
            fd = open(fname, 'w')
            if data:
                fd.write(data)
            fd.close()

        # Create the directory structure
        if not os.path.exists(self.path):
            os.mkdir(self.path)
        os.mkdir(self.get_log_dir())
        os.mkdir(self.get_htdocs_dir())
        os.mkdir(os.path.join(self.path, 'plugins'))

        # Create a few files
        _create_file(os.path.join(self.path, 'VERSION'),
                     'Trac Environment Version 1\n')
        _create_file(os.path.join(self.path, 'README'),
                     'This directory contains a Trac environment.\n'
                     'Visit http://trac.edgewall.org/ for more information.\n')

        # Setup the default configuration
        os.mkdir(os.path.join(self.path, 'conf'))
        _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
        skip_defaults = options and ('inherit', 'file') in [(section, option) \
                for (section, option, value) in options]
        self.setup_config(load_defaults=not skip_defaults)
        for section, name, value in options:
            self.config.set(section, name, value)
        self.config.save()
        self.config.parse_if_needed() # Full reload to get 'inherit' working

        # Create the database
        DatabaseManager(self).init_db()

    def get_version(self, db=None, initial=False):
        """Return the current version of the database.
        If the optional argument `initial` is set to `True`, the version
        of the database used at the time of creation will be returned.

        In practice, for database created before 0.11, this will return `False`
        which is "older" than any db version number.

        :since 0.11:
        """
        if not db:
            db = self.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("SELECT value FROM system "
                       "WHERE name='%sdatabase_version'" %
                       (initial and 'initial_' or ''))
        row = cursor.fetchone()
        return row and int(row[0])

    def setup_config(self, load_defaults=False):
        """Load the configuration file."""
        self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
        if load_defaults:
            for section, default_options in self.config.defaults().items():
                for name, value in default_options.items():
                    if self.config.parent and name in self.config.parent[section]:
                        value = None
                    self.config.set(section, name, value)

    def get_templates_dir(self):
        """Return absolute path to the templates directory."""
        return os.path.join(self.path, 'templates')

    def get_htdocs_dir(self):
        """Return absolute path to the htdocs directory."""
        return os.path.join(self.path, 'htdocs')

    def get_log_dir(self):
        """Return absolute path to the log directory."""
        return os.path.join(self.path, 'log')

    def setup_log(self):
        """Initialize the logging sub-system."""
        from trac.log import logger_factory
        logtype = self.log_type
        logfile = self.log_file
        if logtype == 'file' and not os.path.isabs(logfile):
            logfile = os.path.join(self.get_log_dir(), logfile)
        format = self.log_format
        if format:
            format = format.replace('$(', '%(') \
                     .replace('%(path)s', self.path) \
                     .replace('%(basename)s', os.path.basename(self.path)) \
                     .replace('%(project)s', self.project_name)
        self.log = logger_factory(logtype, logfile, self.log_level, self.path,
                                  format=format)

    def get_known_users(self, cnx=None):
        """Generator that yields information about all known users, i.e. users
        that have logged in to this Trac environment and possibly set their name
        and email.

        This function generates one tuple for every user, of the form
        (username, name, email) ordered alpha-numerically by username.

        @param cnx: the database connection; if ommitted, a new connection is
                    retrieved
        """
        if not cnx:
            cnx = self.get_db_cnx()
        cursor = cnx.cursor()
        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
                       "FROM session AS s "
                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
                       "  and n.authenticated=1 AND n.name = 'name') "
                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
                       "  AND e.authenticated=1 AND e.name = 'email') "
                       "WHERE s.authenticated=1 ORDER BY s.sid")
        for username,name,email in cursor:
            yield username, name, email

    def backup(self, dest=None):
        """Simple SQLite-specific backup of the database.

        @param dest: Destination file; if not specified, the backup is stored in
                     a file called db_name.trac_version.bak
        """
        import shutil

        db_str = self.config.get('trac', 'database')
        if not db_str.startswith('sqlite:'):
            raise TracError(_('Can only backup sqlite databases'))
        db_name = os.path.join(self.path, db_str[7:])
        if not dest:
            dest = '%s.%i.bak' % (db_name, self.get_version())
        shutil.copy (db_name, dest)

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        db = self.get_db_cnx()
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                self.log.warning('Component %s requires environment upgrade',
                                 participant)
                return True
        return False

    def upgrade(self, backup=False, backup_dest=None):
        """Upgrade database.
        
        Each db version should have its own upgrade module, names
        upgrades/dbN.py, where 'N' is the version number (int).

        @param backup: whether or not to backup before upgrading
        @param backup_dest: name of the backup file
        @return: whether the upgrade was performed
        """
        db = self.get_db_cnx()

        upgraders = []
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                upgraders.append(participant)
        if not upgraders:
            return False

        if backup:
            self.backup(backup_dest)
        for participant in upgraders:
            participant.upgrade_environment(db)
        db.commit()

        # Database schema may have changed, so close all connections
        self.shutdown()

        return True

    def _get_href(self):
        if not self._href:
            self._href = Href(urlsplit(self.abs_href.base)[2])
        return self._href
    href = property(_get_href, 'The application root path')

    def _get_abs_href(self):
        if not self._abs_href:
            if not self.base_url:
                self.log.warn('base_url option not set in configuration, '
                              'generated links may be incorrect')
                self._abs_href = Href('')
            else:
                self._abs_href = Href(self.base_url)
        return self._abs_href
    abs_href = property(_get_abs_href, 'The application URL')


class EnvironmentSetup(Component):
    implements(IEnvironmentSetupParticipant)

    # IEnvironmentSetupParticipant methods

    def environment_created(self):
        """Insert default data into the database."""
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        for table, cols, vals in db_default.get_data(db):
            cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table,
                               ','.join(cols), ','.join(['%s' for c in cols])),
                               vals)
        db.commit()
        self._update_sample_config()

    def environment_needs_upgrade(self, db):
        dbver = self.env.get_version(db)
        if dbver == db_default.db_version:
            return False
        elif dbver > db_default.db_version:
            raise TracError(_('Database newer than Trac version'))
        return True

    def upgrade_environment(self, db):
        cursor = db.cursor()
        dbver = self.env.get_version()
        for i in range(dbver + 1, db_default.db_version + 1):
            name  = 'db%i' % i
            try:
                upgrades = __import__('upgrades', globals(), locals(), [name])
                script = getattr(upgrades, name)
            except AttributeError:
                raise TracError(_('No upgrade module for version %(num)i '
                                  '(%(version)s.py)', num=i, version=name))
            script.do_upgrade(self.env, i, cursor)
        cursor.execute("UPDATE system SET value=%s WHERE "
                       "name='database_version'", (db_default.db_version,))
        self.log.info('Upgraded database version from %d to %d',
                      dbver, db_default.db_version)
        self._update_sample_config()

    # Internal methods

    def _update_sample_config(self):
        filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
        config = Configuration(filename)
        for section, default_options in config.defaults().iteritems():
            for name, value in default_options.iteritems():
                config.set(section, name, value)
        try:
            config.save()
            self.log.info('Wrote sample configuration file with the new '
                          'settings and their default values: %s',
                          filename)
        except IOError, e:
            self.log.warn('Couldn\'t write sample configuration file (%s)', e,
                          exc_info=True)


env_cache = {}
env_cache_lock = threading.Lock()

def open_environment(env_path=None, use_cache=False):
    """Open an existing environment object, and verify that the database is up
    to date.

    @param env_path: absolute path to the environment directory; if ommitted,
                     the value of the `TRAC_ENV` environment variable is used
    @param use_cache: whether the environment should be cached for subsequent
                      invocations of this function
    @return: the `Environment` object
    """
    global env_cache, env_cache_lock

    if not env_path:
        env_path = os.getenv('TRAC_ENV')
    if not env_path:
        raise TracError(_('Missing environment variable "TRAC_ENV". '
                          'Trac requires this variable to point to a valid '
                          'Trac environment.'))

    env_path = os.path.normcase(os.path.normpath(env_path))
    if use_cache:
        env_cache_lock.acquire()
        try:
            env = env_cache.get(env_path)
            if env and env.config.parse_if_needed():
                # The environment configuration has changed, so shut it down
                # and remove it from the cache so that it gets reinitialized
                env.log.info('Reloading environment due to configuration '
                             'change')
                env.shutdown()
                if hasattr(env.log, '_trac_handler'):
                    hdlr = env.log._trac_handler
                    env.log.removeHandler(hdlr)
                    hdlr.close()
                del env_cache[env_path]
                env = None
            if env is None:
                env = env_cache.setdefault(env_path, open_environment(env_path))
        finally:
            env_cache_lock.release()
    else:
        env = Environment(env_path)
        needs_upgrade = False
        try:
            needs_upgrade = env.needs_upgrade()
        except Exception, e: # e.g. no database connection
            env.log.exception(e)
        if needs_upgrade:
            raise TracError(_('The Trac Environment needs to be upgraded.\n\n'
                              'Run "trac-admin %(path)s upgrade"',
                              path=env_path))

    return env
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.