예제 #1
0
 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)
예제 #2
0
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
예제 #3
0
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
예제 #4
0
파일: config.py 프로젝트: zxfly/trac
 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
예제 #5
0
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)
예제 #6
0
 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)
예제 #9
0
    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]
예제 #11
0
 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
예제 #12
0
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
예제 #13
0
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
예제 #14
0
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))
예제 #15
0
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
예제 #16
0
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
예제 #17
0
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}
예제 #18
0
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)
예제 #19
0
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)
예제 #20
0
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}
예제 #21
0
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())
예제 #22
0
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
예제 #23
0
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()
예제 #24
0
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)
예제 #25
0
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)
예제 #26
0
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
예제 #27
0
파일: env.py 프로젝트: hanotch/trac
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)
예제 #28
0
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')]
예제 #29
0
파일: api.py 프로젝트: jier38/TracSite
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)
예제 #30
0
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