def list_namespaces(self): providers = ExtensionPoint(IWikiSyntaxProvider).extensions( self.compmgr) for provider in providers: for (namespace, formatter ) in provider.get_link_resolvers(): # @UnusedVariable self.log.debug('namespace: %s' % namespace)
class TagChangeAdminPanel(Component): implements(IAdminPanelProvider) tag_providers = ExtensionPoint(ITagProvider) # AdminPanelProvider methods def get_admin_panels(self, req): if 'TAGS_ADMIN' in req.perm: yield ('tags', _('Tag System'), 'replace', _('Replace')) def render_admin_panel(self, req, cat, page, version): req.perm.require('TAGS_ADMIN') realms = [p.get_taggable_realm() for p in self.tag_providers if (not hasattr(p, 'check_permission') or \ p.check_permission(req.perm, 'view'))] # Check request for enabled filters, or use default. if [r for r in realms if r in req.args] == []: for realm in realms: req.args[realm] = 'on' checked_realms = [r for r in realms if r in req.args] data = dict(checked_realms=checked_realms, tag_realms=list( dict(name=realm, checked=realm in checked_realms) for realm in realms)) tag_system = TagSystem(self.env) if req.method == 'POST': # Replace Tag allow_delete = req.args.get('allow_delete') new_tag = req.args.get('tag_new_name').strip() new_tag = not new_tag == u'' and new_tag or None if not (allow_delete or new_tag): data['error'] = _("Selected current tag(s) and either " "new tag or delete approval are required") else: comment = req.args.get('comment', u'') old_tags = req.args.get('tag_name') if old_tags: # Provide list regardless of single or multiple selection. old_tags = isinstance(old_tags, list) and old_tags or \ [old_tags] tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete, filter=checked_realms) data['selected'] = new_tag query = ' or '.join(['realm:%s' % r for r in checked_realms]) all_tags = sorted(tag_system.get_all_tags(req, query)) data['tags'] = all_tags try: Chrome(self.env).add_textarea_grips(req) except AttributeError: # Element modifiers unavailable before Trac 0.12, skip gracefully. pass return 'admin_tag_change.html', data
class TracFormDBUser(Component): tracformdb_observers = ExtensionPoint(TracFormDBObserver) @tracob_first def save_tracform(self, *_args, **_kw): return self.tracformdb_observers @tracob_first def get_tracform_meta(self, *_args, **_kw): return self.tracformdb_observers @tracob_first def get_tracform_state(self, *_args, **_kw): return self.tracformdb_observers @tracob_first def get_tracform_history(self, *_args, **_kw): return self.tracformdb_observers @tracob_first def get_tracform_fields(self, *_args, **_kw): return self.tracformdb_observers @tracob_first def get_tracform_fieldinfo(self, *_args, **_kw): return self.tracformdb_observers
def __init__(self, section, name, interface, default=None, include_missing=True, doc='', doc_domain='tracini', doc_args=None): ListOption.__init__(self, section, name, default, doc=doc, doc_domain=doc_domain, doc_args=doc_args) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing
class UserExtensiblePermissionStore(Component): """ Default Permission Store extended user permission providers """ implements(IPermissionStore, IPermissionPolicy) user_providers = ExtensionPoint(IPermissionUserProvider) # group_providers = ExtensionPoint(IPermissionGroupProvider) def check_permission(self, action, username, resource, perm): self.log.debug("perm: checking user perms for %s to have %s on %s" % [username, action, resource]) subjects = [] for provider in self.group_providers: subjects.update(provider.get_permission_groups(username)) if action in subjects: return True else: return False def get_user_permissions(self, username): """Retrieve the permissions for the given user and return them in a dictionary. The permissions are stored in the database as (username, action) records. There's simple support for groups by using lowercase names for the action column: such a record represents a group and not an actual permission, and declares that the user is part of that group. Plugins implmenting the IPermissionUserProvider can return permission actions based on user. For example, return TRAC_ADMIN if a user is in a given LDAP group """ self.log.debug("perm: getting user perms") subjects = set([username]) for provider in self.group_providers: subjects.update(provider.get_permission_groups(username)) actions = set([]) for provider in self.user_providers: actions.update(provider.get_permission_action(username)) db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT username,action FROM permission") rows = cursor.fetchall() while True: num_users = len(subjects) num_actions = len(actions) for user, action in rows: if user in subjects: if action.isupper() and action not in actions: actions.add(action) if not action.isupper() and action not in subjects: # action is actually the name of the permission group # here subjects.add(action) if num_users == len(subjects) and num_actions == len(actions): break return list(actions)
def __init__(self, section, name, interface, default=None, doc='', doc_domain='tracini'): Option.__init__(self, section, name, default, doc, doc_domain) self.xtnpt = ExtensionPoint(interface)
class CodeCommentSystem(Component): change_listeners = ExtensionPoint(ICodeCommentChangeListener) def comment_created(self, comment): """ Emits comment_created event to all listeners. """ for listener in self.change_listeners: listener.comment_created(comment)
def GraphDatabaseProperty(resource_uri=None, **params): """Documentation goes here...""" ep = ExtensionPoint(IGraphDatabaseProvider) def GraphDatabaseProperty(component): for neo_service in ep.extensions(component): return neo_service.instance(resource_uri, params) return property(GraphDatabaseProperty)
def __new__(cls, name, bases, class_attrs): interface_class = cls.create_request_filter(name) class_attrs['filters'] = ExtensionPoint(interface_class) new_class = super(HTTPViewRequestFilterCreationMeta, cls).__new__(cls, name, bases, class_attrs) setattr(sys.modules[new_class.__module__], interface_class.__name__, interface_class) return new_class
class WatchlistPreferencePanel(Component): """ Preference panel for editing project watchlist """ implements(IPreferencePanelProvider) # Extension points project_change_listeners = ExtensionPoint(IProjectChangeListener) def get_preference_panels(self, req): if req.authname != 'anonymous': yield ('following', 'Following') def render_preference_panel(self, req, panel): if req.authname == 'anonymous': raise TracError("Can't change preferences!", "No access!") if req.method == 'POST': self.save_watchlist(req) watchlist = [] projects = {} user = get_userstore().getUser(req.authname) w = CQDEWatchlistStore() if user: watchlist = w.get_projects_by_user(user.id) # TODO: inefficient querying for watch in watchlist: projects[watch.project_id] = Project.get(id=watch.project_id) return 'multiproject_user_prefs_watchlist.html', { 'watchlist': watchlist, 'projects' : projects, 'notification_values': w.notifications } def save_watchlist(self, req): user = get_userstore().getUser(req.authname) if not user: return w = CQDEWatchlistStore() if req.args.get('notification'): notifylist = self.__to_list(req.args.get('notification')) for item in notifylist: project_id, notification = item.split('_') w.watch_project(user.id, project_id, notification) # Notify listeners. project = Project.get(id=project_id) for listener in self.project_change_listeners: listener.project_watchers(project) if req.args.get('remove'): removelist = self.__to_list(req.args.get('remove')) for project_id in removelist: w.unwatch_project(user.id, project_id) def __to_list(self, selection): return isinstance(selection, list) and selection or [selection]
def __init__(self, section, name, interface, default=None, include_missing=True, doc=''): ListOption.__init__(self, section, name, default, doc=doc) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing
class XmppPreferencePanel(Component): implements(IAnnouncementPreferenceProvider) formatters = ExtensionPoint(IAnnouncementFormatter) producers = ExtensionPoint(IAnnouncementProducer) distributors = ExtensionPoint(IAnnouncementDistributor) def get_announcement_preference_boxes(self, req): yield 'xmpp', _("XMPP Formats") def render_announcement_preference_box(self, req, panel): supported_realms = {} for producer in self.producers: for realm in producer.realms(): for distributor in self.distributors: for transport in distributor.transports(): for fmtr in self.formatters: for style in fmtr.styles(transport, realm): if realm not in supported_realms: supported_realms[realm] = set() supported_realms[realm].add(style) settings = {} for realm in supported_realms: name = 'xmpp_format_%s' % realm dist = XmppDistributor(self.env).xmpp_format_setting.default settings[realm] = SubscriptionSetting(self.env, name, dist) if req.method == 'POST': for realm, setting in settings.items(): name = 'xmpp_format_%s' % realm setting.set_user_setting(req.session, req.args.get(name), save=False) req.session.save() prefs = {} for realm, setting in settings.items(): prefs[realm] = setting.get_user_setting(req.session.sid)[0] data = dict( realms=supported_realms, preferences=prefs, ) return 'prefs_announcer_xmpp.html', data
class PasswordStoreUser(Component): if can_check_user: passwordstore_observers = ExtensionPoint(IPasswordStore) @tracob_first def has_user(self, *_args, **_kw): return self.passwordstore_observers else: def has_user(self, *_args, **_kw): """Stub, if AccountManagerPlugin isn't installed.""" return False
def resolve_ep_class(interface, component, clsnm, **kwargs): r"""Retrieve the class implementing an interface (by name) """ ep = ExtensionPoint(interface) for c in ep.extensions(component): if c.__class__.__name__ == clsnm: return c else: if 'default' in kwargs: return kwargs['default'] else: raise LookupError('No match found for class %s implementing %s' % (clsnm, interface))
class ClientEventsSystem(Component): summaries = ExtensionPoint(IClientSummaryProvider) actions = ExtensionPoint(IClientActionProvider) def get_summaries(self): for summary in self.summaries: yield summary.get_name() def get_summary(self, name): for summary in self.summaries: if name == summary.get_name(): return summary return None def get_actions(self): for action in self.actions: yield action.get_name() def get_action(self, name): for action in self.actions: if name == action.get_name(): return action return None
class SubscriptionResolver(Component): """Collect, and resolve subscriptions.""" implements(IAnnouncementSubscriptionResolver) subscribers = ExtensionPoint(IAnnouncementSubscriber) def subscriptions(self, event): """Yields all subscriptions for a given event.""" subscriptions = [] for sp in self.subscribers: subscriptions.extend([x for x in sp.matches(event) if x]) """ This logic is meant to generate a list of subscriptions for each distribution method. The important thing is, that we pick the rule with the highest priority for each (sid, distribution) pair. If it is "never", then the user is dropped from the list, if it is "always", then the user is kept. Only users highest priority rule is used and all others are skipped. """ # sort by dist, sid, authenticated, priority subscriptions.sort(key=lambda i: (i[1], i[2], i[3], i[6])) resolved_subs = [] # collect highest priority for each (sid, dist) pair state = {'last': None} for s in subscriptions: if (s[1], s[2], s[3]) == state['last']: continue if s[-1] == 'always': self.log.debug( "Adding (%s [%s]) for 'always' on rule (%s) " "for (%s)", s[2], s[3], s[0], s[1]) resolved_subs.append(s[1:6]) else: self.log.debug( "Ignoring (%s [%s]) for 'never' on rule (%s) " "for (%s)", s[2], s[3], s[0], s[1]) # if s[1] is None, then the subscription is for a raw email # address that has been set in some field and we shouldn't skip # the next raw email subscription. In other words, all raw email # subscriptions should be added. if s[2]: state['last'] = (s[1], s[2], s[3]) return resolved_subs
class AnnouncerPreferences(AnnouncerTemplateProvider): implements(IPreferencePanelProvider) preference_boxes = ExtensionPoint(IAnnouncementPreferenceProvider) # IPreferencePanelProvider methods def get_preference_panels(self, req): if self.preference_boxes: yield ('announcer', _("Announcements")) def _get_boxes(self, req): for pr in self.preference_boxes: boxes = pr.get_announcement_preference_boxes(req) if boxes: for boxname, boxlabel in boxes: if boxname == 'general_wiki' and \ 'WIKI_VIEW' not in req.perm: continue if (boxname == 'legacy' or boxname == 'joinable_groups') and \ 'TICKET_VIEW' not in req.perm: continue yield ((boxname, boxlabel) + pr.render_announcement_preference_box(req, boxname)) def render_preference_panel(self, req, panel, path_info=None): streams = [] chrome = Chrome(self.env) for name, label, template, data in self._get_boxes(req): streams.append((label, chrome.render_template(req, template, data, fragment=True))) if req.method == 'POST': req.redirect(req.href.prefs('announcer')) add_stylesheet(req, 'announcer/css/announcer_prefs.css') if hasattr(chrome, 'jenv'): return 'prefs_announcer.html', {"boxes": streams}, None else: return 'prefs_announcer.html', {"boxes": streams}
class SecurityPreprocessor(Component): participants = ExtensionPoint(ISearchParticipant) def __init__(self): self._required_permissions = {} for participant in self.participants: permission = participant.get_required_permission() doc_type = participant.get_participant_type() self._required_permissions[doc_type] = permission def check_permission(self, doc, context): product, doctype, id = doc['product'], doc['type'], doc['id'] username = context.req.authname env = self.env if product: env = ProductEnvironment(self.env, product) perm = PermissionSystem(env) action = self._required_permissions[doctype] return perm.check_permission(action, username, id) def update_security_filter(self, query_parameters, allowed=(), denied=()): security_filter = self.create_security_filter(query_parameters) security_filter.allowed.extend(allowed) security_filter.denied.extend(denied) def create_security_filter(self, query_parameters): security_filter = self.find_security_filter(query_parameters['filter']) if not security_filter: security_filter = SecurityFilter() if query_parameters['filter']: query_parameters['filter'] = query.And( [query_parameters['filter'], security_filter]) else: query_parameters['filter'] = security_filter return security_filter def find_security_filter(self, existing_query): queue = [existing_query] while queue: token = queue.pop(0) if isinstance(token, SecurityFilter): return token if isinstance(token, query.CompoundQuery): queue.extend(token.subqueries)
class HistoryTaskEvent(Component, ITaskEventListener): """ This task event listener catch task execution to fill all History store in its environment """ implements(ITaskEventListener) history_store_list = ExtensionPoint(IHistoryTaskExecutionStore) def onStartTask(self, task): """ called by the core system when the task is triggered, just before the wake_up method is called """ self.task = task self.start = time() def onEndTask(self, task, success): """ called by the core system when the task execution is finished, just after the task wake_up method exit """ # currently Core assume that task are not threaded so any end event # match the previous start event assert task.getId() == self.task.getId() self.end = time() self.success = success # notify all history store self._notify_history() def getId(self): """ return the id of the listener. It is used in trac.ini """ return "history_task_event" def getDescription(self): return self.__doc__ def _notify_history(self): for historyStore in self.history_store_list: historyStore.addExecution(self.task, self.start, self.end, self.success)
class AnnouncerPreferences(Component): implements(IPreferencePanelProvider, ITemplateProvider) preference_boxes = ExtensionPoint(IAnnouncementPreferenceProvider) def get_htdocs_dirs(self): return [('announcer', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): resource_dir = resource_filename(__name__, 'templates') return [resource_dir] def get_preference_panels(self, req): yield ('announcer', _('Announcements')) def _get_boxes(self, req): for pr in self.preference_boxes: boxes = pr.get_announcement_preference_boxes(req) boxdata = {} if boxes: for boxname, boxlabel in boxes: if boxname == 'general_wiki' and not req.perm.has_permission( 'WIKI_VIEW'): continue if (boxname == 'legacy' or boxname == 'joinable_groups' ) and not req.perm.has_permission('TICKET_VIEW'): continue yield ((boxname, boxlabel) + pr.render_announcement_preference_box(req, boxname)) def render_preference_panel(self, req, panel, path_info=None): streams = [] chrome = Chrome(self.env) for name, label, template, data in self._get_boxes(req): streams.append((label, chrome.render_template(req, template, data, content_type='text/html', fragment=True))) add_stylesheet(req, 'announcer/css/announcer_prefs.css') return 'prefs_announcer.html', {"boxes": streams}
class AlembicMigrator(Component): implements(IEnvironmentSetupParticipant) plugins = ExtensionPoint(IAlembicUpgradeParticipant) disable_upgrade_check = False def environment_created(self): raise NotImplementedError() def environment_needs_upgrade(self, db): if self.disable_upgrade_check: return False for plugin in self.plugins: if is_upgrade_necessary(self.env, db, plugin.script_location()): return True return False def upgrade_environment(self, db): for plugin in self.plugins: run_migrations(self.env, db, plugin.script_location())
class CryptoBase(Component): """Cryptography foundation for Trac.""" _key_stores = ExtensionPoint(IKeyVault) def __init__(self): # Bind 'crypto' catalog to the specified locale directory. locale_dir = resource_filename(__name__, 'locale') add_domain(self.env.path, locale_dir) def keys(self, private=False, id_only=False): """Returns the list of all available keys.""" for store in self._key_stores: for key in store.keys(private=private, id_only=id_only): yield key def has_key(self, key_id, private=False): """Returns whether a key with the given ID is available or not.""" if key_id in self.keys(private=private, id_only=True): return True return False
class BacklogToggleConfigurationJSONView(JSONView): url = '/json/config/backlog/alternative_views' url_regex = '' views = ExtensionPoint(IBacklogToggleViewProvider) def reset_known_backlogs(self): self.ensure_known_backlog_identifiers() # Remove all elements from the array, sneaky I know del self.known_backlog_identifiers()[:] def ensure_known_backlog_identifiers(self): if not hasattr(self, '_known_backlog_identifiers'): self._known_backlog_identifiers = [] def known_backlog_identifiers(self): self.ensure_known_backlog_identifiers() return self._known_backlog_identifiers def register_backlog_with_identifier(self, an_identifier): self.ensure_known_backlog_identifiers() if an_identifier in self.known_backlog_identifiers(): return self.known_backlog_identifiers().append(an_identifier) def register_new_backlog(self): self.register_backlog_with_identifier('new_backlog') def register_whiteboard(self): self.register_backlog_with_identifier('whiteboard') def discover_backlogs(self): for view in self.views: view.register_backlog_for_toggling(self) return self.known_backlog_identifiers() def do_get(self, req, data): return self.discover_backlogs()
class FilesEventNotifier(Component): files_event_listeners = ExtensionPoint(IFilesEventListener) def node_created(self, username, target_node): for listener in self.files_event_listeners: listener.node_created(username, target_node) def node_moved(self, username, target_node, destination_node): for listener in self.files_event_listeners: listener.node_moved(username, target_node, destination_node) def node_copied(self, username, target_node, destination_node): for listener in self.files_event_listeners: listener.node_copied(username, target_node, destination_node) def node_removed(self, username, target_node): for listener in self.files_event_listeners: listener.node_removed(username, target_node) def node_downloaded(self, username, target_node): for listener in self.files_event_listeners: listener.node_downloaded(username, target_node)
class RuleEngine(Component): """ Used to check that all the business rules are met before completing an operation. The RuleEngine has different domains where the rules applies and can be called from the specific object when is saved """ rules = ExtensionPoint(IRule) def __init__(self): """Make sure that all the rules are instantiated and registered""" from agilo.scrum.workflow import rules for member in dir(rules): if type(member) == type and issubclass(member, Component): member(self.env) def validate_rules(self, ticket): """ Validates the give ticket against the registered rules. Every rule will be validated and has to take care of all the checks, return True or False """ debug(self, "Called validate_rules(%s)" % ticket) for r in self.rules: r.validate(ticket)
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled webadmin.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `webadmin` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. 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. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. (''since 0.11.3'')""") project_footer = Option( 'project', 'footer', N_('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. Relative paths are resolved relative to the `log` directory of the environment.""") 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 (see http://docs.python.org/library/logging.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.systeminfo = [] self._href = self._abs_href = None if create: self.create(options) else: self.verify() self.setup_config() if create: for setup_participant in self.setup_participants: setup_participant.environment_created() def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = self.systeminfo[:] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) info.sort(key=lambda (name, version): (name != 'Trac', name.lower())) return info # ISystemInfoProvider methods def get_system_info(self): from trac import core, __version__ as VERSION yield 'Trac', get_pkginfo(core).get('version', VERSION) yield 'Python', sys.version yield 'setuptools', setuptools.__version__ from trac.util.datefmt import pytz if pytz is not None: yield 'pytz', pytz.__version__ 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 _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @property def _component_rules(self): try: return self._rules except AttributeError: self._rules = {} for name, value in self.components_section.options(): if name.endswith('.*'): name = name[:-2] self._rules[name.lower()] = value.lower() in ('enabled', 'on') return self._rules 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 it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) # 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 rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package are enabled return component_name.startswith('trac.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" with open(os.path.join(self.path, 'VERSION'), 'r') as fd: assert fd.read(26) == 'Trac Environment Version 1' def get_db_cnx(self): """Return a database connection from the connection pool :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead `db_transaction` for obtaining the `db` database connection which can be used for performing any query (SELECT/INSERT/UPDATE/DELETE):: with env.db_transaction as db: ... `db_query` for obtaining a `db` database connection which can be used for performing SELECT queries only:: with env.db_query as db: ... """ return DatabaseManager(self).get_connection() def with_transaction(self, db=None): """Decorator for transaction functions :deprecated:""" return with_transaction(self, db) def get_read_db(self): """Return a database connection for read purposes :deprecated: See `trac.db.api.get_read_db` for detailed documentation.""" return DatabaseManager(self).get_connection(readonly=True) @property def db_query(self): """Return a context manager which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: self.log.removeHandler(self._log_handler) self._log_handler.flush() self._log_handler.close() del self._log_handler def get_repository(self, reponame=None, authname=None): """Return the version control repository with the given name, or the default repository if `None`. The standard way of retrieving repositories is to use the methods of `RepositoryManager`. This method is retained here for backward compatibility. :param reponame: the name of the repository :param authname: the user name for authorization (not used anymore, left here for compatibility with 0.11) """ return RepositoryManager(self).get_repository(reponame) 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. """ # 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.sample')) config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # 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 :since 0.13: deprecation warning: the `db` parameter is no longer used and will be removed in version 0.14 """ rows = self.db_query(""" SELECT value FROM system WHERE name='%sdatabase_version' """ % ('initial_' if initial else '')) return rows and int(rows[0][0]) def setup_config(self): """Load the configuration file.""" self.config = Configuration( os.path.join(self.path, 'conf', 'trac.ini'), {'envname': os.path.basename(self.path)}) self.setup_log() from trac.loader import load_components plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) 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_handler_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, self._log_handler = logger_handler_factory(logtype, logfile, self.log_level, self.path, format=format) from trac import core, __version__ as VERSION self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, get_pkginfo(core).get('version', VERSION)) 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 :since 0.13: deprecation warning: the `cnx` parameter is no longer used and will be removed in version 0.14 """ for username, name, email in self.db_query(""" 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 """): yield username, name, email def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" with self.db_query as db: for participant in self.setup_participants: if participant.environment_needs_upgrade(db): self.log.warn("Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] with self.db_query as db: for participant in self.setup_participants: if participant.environment_needs_upgrade(db): upgraders.append(participant) if not upgraders: return if backup: self.backup(backup_dest) for participant in upgraders: self.log.info("%s.%s upgrading...", participant.__module__, participant.__class__.__name__) with self.db_transaction as db: participant.upgrade_environment(db) # Database schema may have changed, so close all connections DatabaseManager(self).shutdown() return True @property def href(self): """The application root path""" if not self._href: self._href = Href(urlsplit(self.abs_href.base)[2]) return self._href @property def abs_href(self): """The application URL""" 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
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled acct_mgr.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `acct_mgr` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. Non-absolute paths are relative to the Environment `conf` directory. """) 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. """) 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. """) anonymous_session_lifetime = IntOption( 'trac', 'anonymous_session_lifetime', '90', """Lifetime of the anonymous session, in days. Set the option to 0 to disable purging old anonymous sessions. (''since 1.0.17'')""") 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. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. """) project_footer = Option( 'project', 'footer', N_('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 = ChoiceOption('logging', 'log_type', log.LOG_TYPES + log.LOG_TYPE_ALIASES, """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""", case_sensitive=False) log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = ChoiceOption('logging', 'log_level', log.LOG_LEVELS + log.LOG_LEVEL_ALIASES, """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`). """, case_sensitive=False) 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 [http://docs.python.org/library/logging.html Python logger library] 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` """) 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 = os.path.normpath(os.path.normcase(path)) self.log = None self.config = None if create: self.create(options) for setup_participant in self.setup_participants: setup_participant.environment_created() else: self.verify() self.setup_config() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.path) @lazy def name(self): """The environment name. :since: 1.2 """ return os.path.basename(self.path) @property def env(self): """Property returning the `Environment` object, which is often required for functions and methods that take a `Component` instance. """ # The cached decorator requires the object have an `env` attribute. return self @property def system_info(self): """List of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = [] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) return sorted(set(info), key=lambda args: (args[0] != 'Trac', args[0].lower())) def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. :since 1.3.1: deprecated and will be removed in 1.5.1. Use system_info property instead. """ return self.system_info # ISystemInfoProvider methods def get_system_info(self): yield 'Trac', self.trac_version yield 'Python', sys.version yield 'setuptools', setuptools.__version__ if pytz is not None: yield 'pytz', pytz.__version__ if hasattr(self, 'webfrontend_version'): yield self.webfrontend, self.webfrontend_version 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 _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @lazy def _component_rules(self): _rules = {} for name, value in self.components_section.options(): name = name.rstrip('.*').lower() _rules[name] = as_bool(value) return _rules 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 it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package except # in trac.test or trac.tests are enabled return component_name.startswith('trac.') and \ not component_name.startswith('trac.test.') and \ not component_name.startswith('trac.tests.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True super(Environment, self).enable_component(cls) @contextmanager def component_guard(self, component, reraise=False): """Traps any runtime exception raised when working with a component and logs the error. :param component: the component responsible for any error that could happen inside the context :param reraise: if `True`, an error is logged but not suppressed. By default, errors are suppressed. """ try: yield except TracError as e: self.log.warning("Component %s failed with %s", component, exception_to_unicode(e)) if reraise: raise except Exception as e: self.log.error("Component %s failed with %s", component, exception_to_unicode(e, traceback=True)) if reraise: raise def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" try: tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] if tag != _VERSION: raise Exception( _("Unknown Trac environment type '%(type)s'", type=tag)) except Exception as e: raise TracError( _("No Trac environment found at %(path)s\n" "%(e)s", path=self.path, e=e)) @lazy def db_exc(self): """Return an object (typically a module) containing all the backend-specific exception types as attributes, named according to the Python Database API (http://www.python.org/dev/peps/pep-0249/). To catch a database exception, use the following pattern:: try: with env.db_transaction as db: ... except env.db_exc.IntegrityError as e: ... """ return DatabaseManager(self).get_exceptions() @property def db_query(self): """Return a context manager (`~trac.db.api.QueryContextManager`) which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still defined, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager (`~trac.db.api.TransactionContextManager`) which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. You should *not* call `commit()` yourself within such block, as this will force a commit even if that transaction is part of a larger transaction. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" from trac.versioncontrol.api import RepositoryManager RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: log.shutdown(self.log) 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. :raises TracError: if the base directory of `path` does not exist. :raises TracError: if `path` exists and is not empty. """ base_dir = os.path.dirname(self.path) if not os.path.exists(base_dir): raise TracError( _( "Base directory '%(env)s' does not exist. Please create it " "and retry.", env=base_dir)) if os.path.exists(self.path) and os.listdir(self.path): raise TracError(_("Directory exists and is not empty.")) # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.htdocs_dir) os.mkdir(self.log_dir) os.mkdir(self.plugins_dir) os.mkdir(self.templates_dir) # Create a few files create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\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(self.conf_dir) config = Configuration(self.config_file_path) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the sample configuration create_file(self.config_file_path + '.sample') self._update_sample_config() # Create the database DatabaseManager(self).init_db() @lazy def database_version(self): """Returns the current version of the database. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('database_version') @lazy def database_initial_version(self): """Returns the version of the database at the time of creation. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('initial_database_version') @lazy def trac_version(self): """Returns the version of Trac. :since: 1.2 """ from trac import core, __version__ return get_pkginfo(core).get('version', __version__) def setup_config(self): """Load the configuration file.""" self.config = Configuration(self.config_file_path, {'envname': self.name}) if not self.config.exists: raise TracError( _("The configuration file is not found at " "%(path)s", path=self.config_file_path)) self.setup_log() plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) @lazy def config_file_path(self): """Path of the trac.ini file.""" return os.path.join(self.conf_dir, 'trac.ini') @lazy def log_file_path(self): """Path to the log file.""" if not os.path.isabs(self.log_file): return os.path.join(self.log_dir, self.log_file) return self.log_file def _get_path_to_dir(self, *dirs): path = self.path for dir in dirs: path = os.path.join(path, dir) return os.path.realpath(path) @lazy def attachments_dir(self): """Absolute path to the attachments directory. :since: 1.3.1 """ return self._get_path_to_dir('files', 'attachments') @lazy def conf_dir(self): """Absolute path to the conf directory. :since: 1.0.11 """ return self._get_path_to_dir('conf') @lazy def files_dir(self): """Absolute path to the files directory. :since: 1.3.2 """ return self._get_path_to_dir('files') @lazy def htdocs_dir(self): """Absolute path to the htdocs directory. :since: 1.0.11 """ return self._get_path_to_dir('htdocs') @lazy def log_dir(self): """Absolute path to the log directory. :since: 1.0.11 """ return self._get_path_to_dir('log') @lazy def plugins_dir(self): """Absolute path to the plugins directory. :since: 1.0.11 """ return self._get_path_to_dir('plugins') @lazy def templates_dir(self): """Absolute path to the templates directory. :since: 1.0.11 """ return self._get_path_to_dir('templates') def setup_log(self): """Initialize the logging sub-system.""" self.log, log_handler = \ self.create_logger(self.log_type, self.log_file_path, self.log_level, self.log_format) self.log.addHandler(log_handler) self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, self.trac_version) def create_logger(self, log_type, log_file, log_level, log_format): log_id = 'Trac.%s' % hashlib.sha1(self.path).hexdigest() if log_format: log_format = log_format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', self.name) \ .replace('%(project)s', self.project_name) return log.logger_handler_factory(log_type, log_file, log_level, log_id, format=log_format) def get_known_users(self, as_dict=False): """Returns information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. By default this function returns a iterator that yields one tuple for every user, of the form (username, name, email), ordered alpha-numerically by username. When `as_dict` is `True` the function returns a dictionary mapping username to a (name, email) tuple. :since 1.2: the `as_dict` parameter is available. """ return self._known_users_dict if as_dict else iter(self._known_users) @cached def _known_users(self): return self.db_query(""" 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 """) @cached def _known_users_dict(self): return {u[0]: (u[1], u[2]) for u in self._known_users} def invalidate_known_users_cache(self): """Clear the known_users cache.""" del self._known_users del self._known_users_dict def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): self.log.warning( "Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): upgraders.append(participant) if not upgraders: return if backup: try: self.backup(backup_dest) except Exception as e: raise BackupError(e) for participant in upgraders: self.log.info("upgrading %s...", participant) with self.component_guard(participant, reraise=True): participant.upgrade_environment() # Database schema may have changed, so close all connections dbm = DatabaseManager(self) if dbm.connection_uri != 'sqlite::memory:': dbm.shutdown() self._update_sample_config() del self.database_version return True @lazy def href(self): """The application root path""" return Href(urlsplit(self.abs_href.base).path) @lazy def abs_href(self): """The application URL""" if not self.base_url: self.log.warning("base_url option not set in configuration, " "generated links may be incorrect") return Href(self.base_url) def _update_sample_config(self): filename = os.path.join(self.config_file_path + '.sample') if not os.path.isfile(filename): return config = Configuration(filename) config.set_defaults() try: config.save() except EnvironmentError as e: self.log.warning("Couldn't write sample configuration file (%s)%s", e, exception_to_unicode(e, traceback=True)) else: self.log.info( "Wrote sample configuration file with the new " "settings and their default values: %s", filename)
class TicketSubmitPolicyPlugin(Component): """ enforce a policy for allowing ticket submission based on fields """ implements(ITemplateStreamFilter, IAdminPanelProvider, ITemplateProvider) # XXX this should be renamed -> actions policies = ExtensionPoint(ITicketSubmitPolicy) comparitors = { 'is': 1, 'is not': 1, 'is in': 'Array', 'is not in': 'Array' } ### methods for accessing the policies # XXX the naming convention for these is horrible def policy_dict(self): retval = {} for policy in self.policies: retval[policy.name()] = policy return retval def save(self, policies): # shorthand section = 'ticket-submit-policy' config = self.env.config # remove the old section for key, value in config.options(section): config.remove(section, key) # create new section from policy dictionary for policy in policies: condition = policies[policy]['condition'] if condition: value = ' && '.join(['%s %s %s' % (i['field'], i['comparitor'], i['value']) for i in condition]) config.set(section, '%s.condition' % policy, value) for action in policies[policy]['actions']: config.set(section, '%s.%s' % (policy, action['name']), ', '.join(action['args'])) # save the policy config.save() def parse(self): """ parse the [ticket-submit-policy] section of the config for policy rules """ section = dict([i for i in self.config.options('ticket-submit-policy')]) def parse_list(string): return [ i.strip() for i in string.split(',') if i.strip()] policies = {} for key in section: try: name, action = key.split('.', 1) except ValueError: self.log.error('invalid key: %s' % key) # XXX log this better continue if not policies.has_key(name): policies[name] = {} if action == 'condition': condition = section[key] conditions = condition.split('&&') for condition in conditions: # look for longest match to prevent substring matching comparitors = self.comparitors.keys() comparitors.sort(key=lambda x: len(x), reverse=True) match = re.match('.* (%s) .*' % '|'.join(comparitors), condition) if match: comparitor = str(match.groups()[0]) # needs to be a str to be JS compatible via repr field, value = [i.strip() for i in condition.split(comparitor, 1)] field = str(field) if self.comparitors[comparitor] == 'Array': value = parse_list(value) else: value = str(value) if 'condition' not in policies[name]: policies[name]['condition'] = [] policies[name]['condition'].append(dict(field=field,value=value,comparitor=comparitor)) else: self.log.error("Invalid condition: %s" % condition) continue if not policies[name].has_key('actions'): policies[name]['actions'] = [] args = parse_list(section[key]) policies[name]['actions'].append({'name': action, 'args': args}) for policy in policies: # empty condition ==> true if not policies[policy].has_key('condition'): policies[policy]['condition'] = [] return policies # method for ITemplateStreamFilter def filter_stream(self, req, method, filename, stream, data): if filename == 'ticket.html': # setup variables javascript = [self.javascript()] onload = [] onsubmit = [] policy_dict = self.policy_dict() # add JS functions to the head block for policy in self.policies: policy_javascript = policy.javascript() if policy_javascript: add_script(req, policy_javascript) # javascript.append(policy_javascript) policies = self.parse() for name, policy in policies.items(): # insert the condition into the JS conditions = policy['condition'] _conditions = [] for condition in conditions: _condition = {} _condition['field'] = condition['field'] _condition['comparitor'] = camelCase(condition['comparitor']) comp_type = self.comparitors[condition['comparitor']] value = condition['value'] if comp_type == 'Array': _condition['value'] = '[ %s ]' % ', '.join(["'%s'" % v for v in value]) else: _condition['value'] = "'%s'" % value _conditions.append("{field: '%(field)s', comparitor: %(comparitor)s, value: %(value)s}" % _condition) condition = '%s = [ %s ];' % (name, ', '.join(_conditions)) javascript.append(condition) # find the correct handler for the policy for action in policy['actions']: handler = policy_dict.get(action['name']) if handler is None: self.log.error('No ITicketSubmitPolicy found for "%s"' % action['name']) continue # filter the stream stream = handler.filter_stream(stream, name, policy['condition'], *action['args']) # add other necessary JS to the page policy_onload = handler.onload(name, policy['condition'], *action['args']) if policy_onload: onload.append(policy_onload) policy_onsubmit = handler.onsubmit(name, policy['condition'], *action['args']) if policy_onsubmit: onsubmit.append(policy_onsubmit) # insert onload, onsubmit hooks if supplied if onload: javascript.append(self.onload(onload)) if onsubmit: javascript.append(self.onsubmit(onsubmit)) stream |= Transformer("//form[@id='propertyform']").attr('onsubmit', 'validate()') # insert head javascript if javascript: javascript = '\n%s\n' % '\n'.join(javascript) javascript = tag.script(Markup(javascript), **{ "type": "text/javascript"}) stream |= Transformer("head").append(javascript) return stream ### methods returning JS ### TODO: convert these all to templates def onload(self, items): return """ $(document).ready(function () { %s }); """ % '\n'.join(items) def onsubmit(self, items): """returns text for the onsubmit JS function to be inserted in the head""" message = """message = %s if (message != true) { errors[errors.length] = message; } """ messages = '\n'.join([(message % item) for item in items]) return """ function validate() { var errors = new Array(); %s if (errors.length) { if (errors.length == 1) { error_msg = errors[0]; } else { error_msg = errors.join("\\n"); } alert(error_msg); return false; } return true; } """ % messages def javascript(self): """head javascript required to enforce ticket submission policy""" # XXX this should probably go into a separate file string = """ function getValue(id) { var x=document.getElementById(id); return x.options[x.selectedIndex].text; } function is(x, y) { return (x == y); } function isNot(x, y) { return (x != y); } function isIn(x, y) { for (index in y) { if(x == y[index]) { return true; } } return false; } function isNotIn(x, y) { return !isIn(x,y); } function condition(policy) { length = policy.length; for ( var i=0; i != length; i++ ) { field = getValue('field-' + policy[i].field); comparitor = policy[i].comparitor; value = policy[i].value; if ( !comparitor(field, value) ) { return false; } } return true; } function policytostring(policy) { // this should be replaced by a deCamelCase function names = { is: 'is', isNot: 'is not', isIn: 'is in', isNotIn: 'is not in' } var strings = new Array(policy.length); for ( var i=0; i != policy.length; i++ ) { funcname = names[policy[i].comparitor.name]; strings[i] = policy[i].field + ' ' + funcname + ' ' + policy[i].value; } return strings.join(' and '); } """ return string ### methods for IAdminPanelProvider def get_admin_panels(self, req): """Return a list of available admin panels. The items returned by this function must be tuples of the form `(category, category_label, page, page_label)`. """ if req.perm.has_permission('TRAC_ADMIN'): yield ('ticket', 'Ticket System', 'policy', 'Submit Policy') def render_admin_panel(self, req, category, page, path_info): """Process a request for an admin panel. This function should return a tuple of the form `(template, data)`, where `template` is the name of the template to use and `data` is the data to be passed to the template. """ data = {} # data for template data['fields'] = Ticket(self.env).fields # possible ticket fields data['comparitors'] = self.comparitors # implemented comparitors data['self_actions'] = self.policies # available policies data['saved'] = True if req.method == 'POST': # mark the page as unsaved data['saved'] = False # organize request args based on policy policies = req.args.get('policy', []) if isinstance(policies, basestring): policies = [ policies ] args = dict([(policy, {}) for policy in policies ]) for arg, value in req.args.items(): for policy in policies: token = '_%s' % policy if arg.endswith(token): args[policy][arg.rsplit(token, 1)[0]] = value # get the conditions and policies from the request data['policies'] = {} for policy in args: if 'remove' in args[policy]: continue ### remove this policy conditions = {} data['policies'][policy] = dict(condition=[], actions=[]) for field, value in args[policy].items(): token = 'condition_' if field.startswith(token): name, index = field.split(token, 1)[1].rsplit('_', 1) index = int(index) if not conditions.has_key(index): conditions[index] = {} conditions[index][name] = value continue token = 'action_' if field.startswith(token): name = field.split(token, 1)[1] if args[policy].get('rm_action_%s' % name, False): continue if isinstance(value, basestring): value = [ value ] data['policies'][policy]['actions'].append(dict(name=name, args=value)) # added action new_action = args[policy].get('add_action') if new_action: data['policies'][policy]['actions'].append(dict(name=new_action, args=[])) for index in sorted(conditions.keys()): if 'rm_condition_%s' % index not in args[policy]: data['policies'][policy]['condition'].append(conditions[index]) # added conditions new_conditions = [ i.split('add_condition_', 1)[-1] for i in req.args.keys() if i.startswith('add_condition_') ] for name in new_conditions: data['policies'][name]['condition'].append(dict(comparitor='', field='', value='')) # added policy new_policy = req.args.get('new-policy') if new_policy: data['policies'][new_policy] = dict(actions=[], condition=[]) # save the data if the user clicks apply if 'apply' in req.args: self.save(data['policies']) data['saved'] = True data['current_policy'] = '[ticket-submit-policy]\n%s' % '\n'.join(['%s = %s' % val for val in self.env.config.options('ticket-submit-policy')]) if req.method == 'GET' or 'apply' in req.args: data['policies'] = self.parse() return ('ticketsubmitpolicy.html', data) ### methods for ITemplateProvider def get_htdocs_dirs(self): """Return a list of directories with static resources (such as style sheets, images, etc.) Each item in the list must be a `(prefix, abspath)` tuple. The `prefix` part defines the path in the URL that requests to these resources are prefixed with. The `abspath` is the absolute path to the directory containing the resources on the local file system. """ return [('ticketsubmitpolicy', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): """Return a list of directories containing the provided template files. """ return [resource_filename(__name__, 'templates')]
class TagSystem(Component): """[main] Tagging system for Trac. Associating resources with tags is easy, faceted content classification. Available components are marked according to their relevance as follows: `[main]`:: provide core with a generic tagging engine as well as support for common realms 'ticket' (TracTickets) and 'wiki' (TracWiki). `[opt]`:: add more features to improve user experience and maintenance `[extra]`:: enable advanced features for specific use cases Make sure to understand their purpose before deactivating `[main]` or activating `[extra]` components. """ implements(IPermissionRequestor, IResourceManager) tag_providers = ExtensionPoint(ITagProvider) revisable = ListOption( 'tags', 'revisable_realms', 'wiki', doc="Comma-separated list of realms requiring tag change history.") wiki_page_link = BoolOption( 'tags', 'wiki_page_link', True, doc="Link a tag to the wiki page with same name, if it exists.") wiki_page_prefix = Option('tags', 'wiki_page_prefix', '', doc="Prefix for tag wiki page names.") # Internal variables _realm_provider_map = None def __init__(self): # Bind the 'tractags' catalog to the specified locale directory. locale_dir = pkg_resources.resource_filename(__name__, 'locale') add_domain(self.env.path, locale_dir) self._populate_provider_map() # Public methods def query(self, req, query='', attribute_handlers=None): """Returns a sequence of (resource, tags) tuples matching a query. Query syntax is described in tractags.query. :param attribute_handlers: Register additional query attribute handlers. See Query documentation for more information. """ def realm_handler(_, node, context): return query.match(node, [context.realm]) all_attribute_handlers = { 'realm': realm_handler, } all_attribute_handlers.update(attribute_handlers or {}) query = Query(query, attribute_handlers=all_attribute_handlers) providers = set() for m in REALM_RE.finditer(query.as_string()): realm = m.group(1) providers.add(self._get_provider(realm)) if not providers: providers = self.tag_providers query_tags = set(query.terms()) for provider in providers: self.env.log.debug('Querying ' + repr(provider)) for resource, tags in provider.get_tagged_resources( req, query_tags) or []: if query(tags, context=resource): yield resource, tags def get_taggable_realms(self, perm=None): """Returns the names of available taggable realms as set. If a `PermissionCache` object is passed as optional `perm` argument, permission checks will be done for tag providers that have a `check_permission` method. """ return set(p.get_taggable_realm() for p in self.tag_providers if perm is None or not hasattr(p, 'check_permission') or p.check_permission(perm, 'view')) def get_all_tags(self, req, realms=[]): """Get all tags for all supported realms or only for specified ones. Returns a Counter object (special dict) with tag name as key and tag frequency as value. """ all_tags = collections.Counter() all_realms = self.get_taggable_realms(req.perm) if not realms or set(realms) == all_realms: realms = all_realms for provider in self.tag_providers: if provider.get_taggable_realm() in realms: try: all_tags += provider.get_all_tags(req) except AttributeError: # Fallback for older providers. try: for resource, tags in \ provider.get_tagged_resources(req): all_tags.update(tags) except TypeError: # Defense against loose ITagProvider implementations, # that might become obsolete in the future. self.env.log.warning('ITagProvider %r has outdated' 'get_tagged_resources() method' % provider) return all_tags def get_tags(self, req, resource, when=None): """Get tags for resource.""" if not req: # Bypass permission checks as required i. e. for TagsPolicy, # an IPermissionProvider. return set(self._get_provider(resource.realm) \ .resource_tags(resource)) return set(self._get_provider(resource.realm) \ .get_resource_tags(req, resource, when=when)) def set_tags(self, req, resource, tags, comment=u'', when=None): """Set tags on a resource. Existing tags are replaced. """ try: return self._get_provider(resource.realm) \ .set_resource_tags(req, resource, set(tags), comment, when) except TypeError: # Handle old style tag providers gracefully. return self._get_provider(resource.realm) \ .set_resource_tags(req, resource, set(tags)) def add_tags(self, req, resource, tags, comment=u''): """Add to existing tags on a resource.""" tags = set(tags) tags.update(self.get_tags(req, resource)) try: self.set_tags(req, resource, tags, comment) except TypeError: # Handle old style tag providers gracefully. self.set_tags(req, resource, tags) def reparent_tags(self, req, resource, old_name, comment=u''): """Move tags, typically when renaming an existing resource. Tags can't be moved between different tag realms with intention. """ provider = self._get_provider(resource.realm) provider.reparent_resource_tags(req, resource, old_name, comment) def replace_tag(self, req, old_tags, new_tag=None, comment=u'', allow_delete=False, filter=[]): """Replace one or more tags in all resources it exists/they exist in. Tagged resources may be filtered by realm and tag deletion is optionally allowed for convenience as well. """ # Provide list regardless of attribute type. for provider in [ p for p in self.tag_providers if not filter or p.get_taggable_realm() in filter ]: for resource, tags in \ provider.get_tagged_resources(req, old_tags): old_tags = set(old_tags) if old_tags.issuperset(tags) and not new_tag: if allow_delete: self.delete_tags(req, resource, None, comment) else: s_tags = set(tags) eff_tags = s_tags - old_tags if new_tag: eff_tags.add(new_tag) # Prevent to touch resources without effective change. if eff_tags != s_tags and (allow_delete or new_tag): self.set_tags(req, resource, eff_tags, comment) def delete_tags(self, req, resource, tags=None, comment=u''): """Delete tags on a resource. If tags is None, remove all tags on the resource. """ provider = self._get_provider(resource.realm) if tags is None: try: provider.remove_resource_tags(req, resource, comment) except TypeError: # Handle old style tag providers gracefully. provider.remove_resource_tags(req, resource) else: current_tags = set(provider.get_resource_tags(req, resource)) current_tags.difference_update(tags) try: provider.set_resource_tags(req, resource, current_tags, comment) except TypeError: # Handle old style tag providers gracefully. provider.set_resource_tags(req, resource, current_tags) def describe_tagged_resource(self, req, resource): """Returns a short description of a taggable resource.""" provider = self._get_provider(resource.realm) try: return provider.describe_tagged_resource(req, resource) except (AttributeError, NotImplementedError): # Fallback to resource provider method. self.env.log.info('ITagProvider %r does not implement ' 'describe_tagged_resource()' % provider) return get_resource_description(self.env, resource, 'summary') # IPermissionRequestor method def get_permission_actions(self): action = ['TAGS_VIEW', 'TAGS_MODIFY'] actions = [action[0], (action[1], [action[0]]), ('TAGS_ADMIN', action)] return actions # IResourceManager methods def get_resource_realms(self): yield 'tag' def get_resource_url(self, resource, href, form_realms=None, **kwargs): if self.wiki_page_link: page = WikiPage(self.env, self.wiki_page_prefix + resource.id) if page.exists: return get_resource_url(self.env, page.resource, href, **kwargs) if form_realms: return href.tags(form_realms, q=to_unicode(resource.id), **kwargs) return href.tags(to_unicode(resource.id), form_realms, **kwargs) def get_resource_description(self, resource, format='default', context=None, **kwargs): if self.wiki_page_link: page = WikiPage(self.env, self.wiki_page_prefix + resource.id) if page.exists: return get_resource_description(self.env, page.resource, format, **kwargs) rid = to_unicode(resource.id) if format in ('compact', 'default'): return rid else: return u'tag:%s' % rid # Internal methods def _populate_provider_map(self): if self._realm_provider_map is None: # Only use the map once it is fully initialized. map = dict((provider.get_taggable_realm(), provider) for provider in self.tag_providers) self._realm_provider_map = map def _get_provider(self, realm): try: return self._realm_provider_map[realm] except KeyError: raise InvalidTagRealm( _("Tags are not supported on the '%s' realm") % realm)
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 wiki macros. * wiki and ticket attachments. """ setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) def __init__(self, path, create=False, db_str=None): """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 db_str: the database connection string """ ComponentManager.__init__(self) self.path = path self.__cnx_pool = None if create: self.create(db_str) else: self.verify() self.load_config() self.setup_log() from trac.loader import load_components load_components(self) 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, (str, unicode)): 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]): return enabled # 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') assert fd.read(26) == 'Trac Environment Version 1' fd.close() def get_db_cnx(self): """Return a database connection from the connection pool.""" if not self.__cnx_pool: self.__cnx_pool = db.get_cnx_pool(self) return self.__cnx_pool.get_cnx() def shutdown(self): """Close the environment.""" if self.__cnx_pool: self.__cnx_pool.shutdown() self.__cnx_pool = None def get_repository(self, authname=None): """Return the version control repository configured for this environment. The repository is wrapped in a `CachedRepository`. @param authname: user name for authorization """ from trac.versioncontrol.cache import CachedRepository from trac.versioncontrol.svn_authz import SubversionAuthorizer from trac.versioncontrol.svn_fs import SubversionRepository repos_dir = self.config.get('trac', 'repository_dir') if not repos_dir: raise EnvironmentError, 'Path to repository not configured' authz = None if authname: authz = SubversionAuthorizer(self, authname) repos = SubversionRepository(repos_dir, authz, self.log) return CachedRepository(self.get_db_cnx(), repos, authz, self.log) def create(self, db_str=None): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values.""" def _create_file(fname, data=None): fd = open(fname, 'w') if data: fd.write(data) fd.close() # Create the directory structure os.mkdir(self.path) os.mkdir(self.get_log_dir()) os.mkdir(self.get_htdocs_dir()) os.mkdir(os.path.join(self.path, 'plugins')) os.mkdir(os.path.join(self.path, 'wiki-macros')) # 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.com/ 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')) self.load_config() for section, name, value in db_default.default_config: self.config.set(section, name, value) self.config.set('trac', 'database', db_str) self.config.save() # Create the database db.init_db(self.path, db_str) def get_version(self, db=None): """Return the current version of the database.""" if not db: db = self.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT value FROM system WHERE name='database_version'") row = cursor.fetchone() return row and int(row[0]) def load_config(self): """Load the configuration file.""" self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) for section, name, value in db_default.default_config: self.config.setdefault(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.config.get('logging', 'log_type') loglevel = self.config.get('logging', 'log_level') logfile = self.config.get('logging', 'log_file') if not os.path.isabs(logfile): logfile = os.path.join(self.get_log_dir(), logfile) logid = self.path # Env-path provides process-unique ID self.log = logger_factory(logtype, logfile, loglevel, logid) 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.var_value, e.var_value " "FROM session AS s " " LEFT JOIN session AS n ON (n.sid=s.sid " " AND n.authenticated=1 AND n.var_name = 'name') " " LEFT JOIN session AS e ON (e.sid=s.sid " " AND e.authenticated=1 AND e.var_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 EnvironmentError, '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