示例#1
0
    def expand_macro(self, formatter, name, args):
        from trac.config import ConfigSection, Option
        section_filter = key_filter = ''
        args, kw = parse_args(args)
        if args:
            section_filter = args.pop(0).strip()
        if args:
            key_filter = args.pop(0).strip()

        def getdoc(option_or_section):
            doc = to_unicode(option_or_section.__doc__)
            if doc:
                doc = dgettext(option_or_section.doc_domain, doc)
            return doc

        registry = ConfigSection.get_registry(self.compmgr)
        sections = dict((name, getdoc(section))
                        for name, section in registry.iteritems()
                        if name.startswith(section_filter))

        registry = Option.get_registry(self.compmgr)
        options = {}
        for (section, key), option in registry.iteritems():
            if section.startswith(section_filter):
                options.setdefault(section, {})[key] = option
                sections.setdefault(section, '')

        def default_cell(option):
            default = option.default
            if default is True:
                default = 'true'
            elif default is False:
                default = 'false'
            elif default == 0:
                default = '0.0' if isinstance(default, float) else '0'
            elif default:
                default = ', '.join(to_unicode(val) for val in default) \
                          if isinstance(default, (list, tuple)) \
                          else to_unicode(default)
            else:
                return tag.td(_("(no default)"), class_='nodefault')
            return tag.td(tag.code(default), class_='default')

        return tag.div(class_='tracini')(
            (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
             format_to_html(self.env, formatter.context, section_doc),
             tag.table(class_='wiki')(tag.tbody(
                 tag.tr(tag.td(tag.tt(option.name)),
                        tag.td(format_to_oneliner(
                            self.env, formatter.context, getdoc(option))),
                        default_cell(option))
                 for option in sorted(options.get(section, {}).itervalues(),
                                      key=lambda o: o.name)
                 if option.name.startswith(key_filter))))
            for section, section_doc in sorted(sections.iteritems()))
示例#2
0
文件: macros.py 项目: exocad/exotrac
    def expand_macro(self, formatter, name, content):
        from trac.config import ConfigSection, Option
        section_filter = key_filter = ''
        args, kw = parse_args(content)
        if args:
            section_filter = args.pop(0).strip()
        if args:
            key_filter = args.pop(0).strip()

        def getdoc(option_or_section):
            doc = to_unicode(option_or_section.__doc__)
            if doc:
                doc = dgettext(option_or_section.doc_domain, doc)
            return doc

        registry = ConfigSection.get_registry(self.compmgr)
        sections = dict((name, getdoc(section))
                        for name, section in registry.iteritems()
                        if name.startswith(section_filter))

        registry = Option.get_registry(self.compmgr)
        options = {}
        for (section, key), option in registry.iteritems():
            if section.startswith(section_filter):
                options.setdefault(section, {})[key] = option
                sections.setdefault(section, '')

        def default_cell(option):
            default = option.default
            if default is not None and default != '':
                return tag.td(tag.code(option.dumps(default)),
                              class_='default')
            else:
                return tag.td(_("(no default)"), class_='nodefault')

        return tag.div(class_='tracini')(
            (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
             format_to_html(self.env, formatter.context, section_doc),
             tag.table(class_='wiki')(tag.tbody(
                 tag.tr(tag.td(tag.tt(option.name)),
                        tag.td(format_to_oneliner(
                            self.env, formatter.context, getdoc(option))),
                        default_cell(option),
                        class_='odd' if idx % 2 else 'even')
                 for idx, option in
                    enumerate(sorted(options.get(section, {}).itervalues(),
                                     key=lambda o: o.name))
                 if option.name.startswith(key_filter))))
            for section, section_doc in sorted(sections.iteritems()))
示例#3
0
    def expand_macro(self, formatter, name, args):
        from trac.config import ConfigSection, Option
        section_filter = key_filter = ''
        args, kw = parse_args(args)
        if args:
            section_filter = args.pop(0).strip()
        if args:
            key_filter = args.pop(0).strip()

        registry = ConfigSection.get_registry(self.compmgr)
        sections = dict(
            (name, dgettext(section.doc_domain, to_unicode(section.__doc__)))
            for name, section in registry.iteritems()
            if name.startswith(section_filter))

        registry = Option.get_registry(self.compmgr)
        options = {}
        for (section, key), option in registry.iteritems():
            if section.startswith(section_filter):
                options.setdefault(section, {})[key] = option
                sections.setdefault(section, '')

        return tag.div(class_='tracini')(
            (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
             format_to_html(self.env, formatter.context, section_doc),
             tag.table(class_='wiki')(tag.tbody(
                 tag.tr(
                     tag.td(tag.tt(option.name)),
                     tag.td(
                         format_to_oneliner(
                             self.env, formatter.context,
                             dgettext(option.doc_domain,
                                      to_unicode(option.__doc__)))),
                     tag.td(
                         tag.code(option.default or 'false') if option.default
                         or option.default is False else _("(no default)"),
                         class_='default' if option.default
                         or option.default is False else 'nodefault'))
                 for option in sorted(options.get(section, {}).itervalues(),
                                      key=lambda o: o.name)
                 if option.name.startswith(key_filter))))
            for section, section_doc in sorted(sections.iteritems()))
示例#4
0
    def expand_macro(self, formatter, name, args):
        from trac.config import ConfigSection, Option
        section_filter = key_filter = ''
        args, kw = parse_args(args)
        if args:
            section_filter = args.pop(0).strip()
        if args:
            key_filter = args.pop(0).strip()

        registry = ConfigSection.get_registry(self.compmgr)
        sections = dict((name, dgettext(section.doc_domain,
                                        to_unicode(section.__doc__)))
                        for name, section in registry.iteritems()
                        if name.startswith(section_filter))

        registry = Option.get_registry(self.compmgr)
        options = {}
        for (section, key), option in registry.iteritems():
            if section.startswith(section_filter):
                options.setdefault(section, {})[key] = option
                sections.setdefault(section, '')

        return tag.div(class_='tracini')(
            (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
             format_to_html(self.env, formatter.context, section_doc),
             tag.table(class_='wiki')(tag.tbody(
                 tag.tr(tag.td(tag.tt(option.name)),
                        tag.td(format_to_oneliner(
                            self.env, formatter.context,
                            dgettext(option.doc_domain,
                                     to_unicode(option.__doc__)))),
                        tag.td(tag.code(option.default or 'false')
                                   if option.default or option.default is False
                                   else _("(no default)"),
                               class_='default' if option.default or 
                                                   option.default is False 
                                                else 'nodefault'))
                 for option in sorted(options.get(section, {}).itervalues(),
                                      key=lambda o: o.name)
                 if option.name.startswith(key_filter))))
            for section, section_doc in sorted(sections.iteritems()))
示例#5
0
class ExtraPermissionsProvider(Component):
    """Extra permission provider."""

    implements(IPermissionRequestor)

    extra_permissions_section = ConfigSection(
        'extra-permissions',
        doc="""This section provides a way to add arbitrary permissions to a
        Trac environment. This can be useful for adding new permissions to use
        for workflow actions, for example.

        To add new permissions, create a new section `[extra-permissions]` in
        your `trac.ini`. Every entry in that section defines a meta-permission
        and a comma-separated list of permissions. For example:
        {{{
        [extra-permissions]
        extra_admin = extra_view, extra_modify, extra_delete
        }}}
        This entry will define three new permissions `EXTRA_VIEW`,
        `EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions
        `EXTRA_ADMIN` that grants all three permissions.

        If you don't want a meta-permission, start the meta-name with an
        underscore (`_`):
        {{{
        [extra-permissions]
        _perms = extra_view, extra_modify
        }}}
        """)

    def get_permission_actions(self):
        permissions = {}
        for meta, perms in self.extra_permissions_section.options():
            perms = [each.strip().upper() for each in perms.split(',')]
            for perm in perms:
                permissions.setdefault(perm, [])
            meta = meta.strip().upper()
            if meta and not meta.startswith('_'):
                permissions.setdefault(meta, []).extend(perms)
        return [(k, v) if v else k for k, v in permissions.iteritems()]
示例#6
0
    def render_admin_panel(self, req, cat, page, path_info):
        req.perm.require("TRAC_ADMIN")

        if path_info == None:
            ext = ""
        else:
            ext = "/" + path_info

        #
        # Gather section names for section drop down field
        #
        all_section_names = []
        for section_name in self.config.sections():
            if section_name == "components":
                continue
            all_section_names.append(section_name)

        # Check whether section exists and if it's not existing then check whether
        # its name is a valid section name.
        if (
            (path_info is not None)
            and (path_info not in ("", "/", "_all_sections"))
            and (path_info not in all_section_names)
        ):
            if path_info == "components":
                add_warning(req, _('The section "components" can\'t be edited with the ini editor.'))
                req.redirect(req.href.admin(cat, page))
                return None
            elif self.valid_section_name_chars_regexp.match(path_info) is None:
                add_warning(req, _("The section name %s is invalid.") % path_info)
                req.redirect(req.href.admin(cat, page))
                return None

            # Add current section if it's not already in the list. This happens if
            # the section is essentially empty (i.e. newly created with no non-default
            # option values and no option from the option registry).
            all_section_names.append(path_info)

        registry = ConfigSection.get_registry(self.compmgr)
        descriptions = {}
        for section_name, section in registry.items():
            if section_name == "components":
                continue
            doc = section.__doc__
            if not section_name in all_section_names:
                all_section_names.append(section_name)
            if doc:
                descriptions[section_name] = dgettext(section.doc_domain, doc)

        all_section_names.sort()

        sections = {}

        #
        # Check security manager
        #
        manager = None
        try:
            manager = self.security_manager
        except Exception, detail:  # "except ... as ..." is only available since Python 2.6
            if req.method != "POST":
                # only add this warning once
                add_warning(req, _("Security manager could not be initated. %s") % unicode(detail))
示例#7
0
class DefaultTicketGroupStatsProvider(Component):
    """Configurable ticket group statistics provider.

    See :teo:`TracIni#milestone-groups-section` for a detailed
    example configuration.
    """

    implements(ITicketGroupStatsProvider)

    milestone_groups_section = ConfigSection(
        'milestone-groups',
        """As the workflow for tickets is now configurable, there can
        be many ticket states, and simply displaying closed tickets
        vs. all the others is maybe not appropriate in all cases. This
        section enables one to easily create ''groups'' of states that
        will be shown in different colors in the milestone progress
        bar.

        Note that the groups can only be based on the ticket
        //status//, nothing else. In particular, it's not possible to
        distinguish between different closed tickets based on the
        //resolution//.

        Example configuration with three groups, //closed//, //new//
        and //active// (the default only has closed and active):
        {{{
        # the 'closed' group correspond to the 'closed' tickets
        closed = closed

        # .order: sequence number in the progress bar
        closed.order = 0

        # .query_args: optional parameters for the corresponding
        #              query.  In this example, the changes from the
        #              default are two additional columns ('created' and
        #              'modified'), and sorting is done on 'created'.
        closed.query_args = group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime

        # .overall_completion: indicates groups that count for overall
        #                      completion percentage
        closed.overall_completion = true

        new = new
        new.order = 1
        new.css_class = new
        new.label = new

        # Note: one catch-all group for other statuses is allowed
        active = *
        active.order = 2

        # .css_class: CSS class for this interval
        active.css_class = open

        # .label: displayed label for this group
        active.label = in progress
        }}}

        The definition consists in a comma-separated list of accepted
        status.  Also, '*' means any status and could be used to
        associate all remaining states to one catch-all group.

        The CSS class can be one of: new (yellow), open (no color) or
        closed (green). Other styles can easily be added using custom
        CSS rule: `table.progress td.<class> { background: <color> }`
        to a [TracInterfaceCustomization#SiteAppearance site/style.css] file
        for example.
        """)

    default_milestone_groups = [{
        'name': 'closed',
        'status': 'closed',
        'query_args': 'group=resolution',
        'overall_completion': 'true'
    }, {
        'name': 'active',
        'status': '*',
        'css_class': 'open'
    }]

    def _get_ticket_groups(self):
        """Returns a list of dict describing the ticket groups
        in the expected order of appearance in the milestone progress bars.
        """
        if 'milestone-groups' in self.config:
            groups = {}
            order = 0
            for groupname, value in self.milestone_groups_section.options():
                qualifier = 'status'
                if '.' in groupname:
                    groupname, qualifier = groupname.split('.', 1)
                group = groups.setdefault(groupname, {
                    'name': groupname,
                    'order': order
                })
                group[qualifier] = value
                order = max(order, int(group['order'])) + 1
            return [
                group for group in sorted(groups.values(),
                                          key=lambda g: int(g['order']))
            ]
        else:
            return self.default_milestone_groups

    def get_ticket_group_stats(self, ticket_ids):
        total_cnt = len(ticket_ids)
        all_statuses = set(TicketSystem(self.env).get_all_status())
        status_cnt = {}
        for s in all_statuses:
            status_cnt[s] = 0
        if total_cnt:
            for status, count in self.env.db_query("""
                    SELECT status, count(status) FROM ticket
                    WHERE id IN (%s) GROUP BY status
                    """ % ",".join(str(x) for x in sorted(ticket_ids))):
                status_cnt[status] = count

        stat = TicketGroupStats(_("ticket status"), _("tickets"))
        remaining_statuses = set(all_statuses)
        groups = self._get_ticket_groups()
        catch_all_group = None
        # we need to go through the groups twice, so that the catch up group
        # doesn't need to be the last one in the sequence
        for group in groups:
            status_str = group['status'].strip()
            if status_str == '*':
                if catch_all_group:
                    raise TracError(
                        _(
                            "'%(group1)s' and '%(group2)s' milestone groups "
                            "both are declared to be \"catch-all\" groups. "
                            "Please check your configuration.",
                            group1=group['name'],
                            group2=catch_all_group['name']))
                catch_all_group = group
            else:
                group_statuses = {s.strip() for s in status_str.split(',')} \
                                 & all_statuses
                if group_statuses - remaining_statuses:
                    raise TracError(
                        _(
                            "'%(groupname)s' milestone group reused status "
                            "'%(status)s' already taken by other groups. "
                            "Please check your configuration.",
                            groupname=group['name'],
                            status=', '.join(group_statuses -
                                             remaining_statuses)))
                else:
                    remaining_statuses -= group_statuses
                group['statuses'] = group_statuses
        if catch_all_group:
            catch_all_group['statuses'] = remaining_statuses
        for group in groups:
            group_cnt = 0
            query_args = {}
            for s, cnt in status_cnt.iteritems():
                if s in group['statuses']:
                    group_cnt += cnt
                    query_args.setdefault('status', []).append(s)
            for arg in [
                    kv for kv in group.get('query_args', '').split(',')
                    if '=' in kv
            ]:
                k, v = [a.strip() for a in arg.split('=', 1)]
                query_args.setdefault(k, []).append(v)
            stat.add_interval(group.get('label', group['name']),
                              group_cnt, query_args,
                              group.get('css_class', group['name']),
                              as_bool(group.get('overall_completion')))
        stat.refresh_calcs()
        return stat
示例#8
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)
示例#9
0
class InterWikiMap(Component):
    """InterWiki map manager."""

    implements(IWikiChangeListener, IWikiMacroProvider)

    interwiki_section = ConfigSection(
        'interwiki',
        """Every option in the `[interwiki]` section defines one InterWiki
        prefix. The option name defines the prefix. The option value defines
        the URL, optionally followed by a description separated from the URL
        by whitespace. Parametric URLs are supported as well.

        '''Example:'''
        {{{
        [interwiki]
        MeatBall = http://www.usemod.com/cgi-bin/mb.pl?
        PEP = http://www.python.org/peps/pep-$1.html Python Enhancement Proposal $1
        tsvn = tsvn: Interact with TortoiseSvn
        }}}
        """)

    _page_name = 'InterMapTxt'
    _interwiki_re = re.compile(
        r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" % WikiParser.LINK_SCHEME,
        re.UNICODE)
    _argspec_re = re.compile(r"\$\d")

    # The component itself behaves as a read-only map

    def __contains__(self, ns):
        return ns.upper() in self.interwiki_map

    def __getitem__(self, ns):
        return self.interwiki_map[ns.upper()]

    def keys(self):
        return list(self.interwiki_map)

    # Expansion of positional arguments ($1, $2, ...) in URL and title
    def _expand(self, txt, args):
        """Replace "$1" by the first args, "$2" by the second, etc."""
        def setarg(match):
            num = int(match.group()[1:])
            return args[num - 1] if 0 < num <= len(args) else ''

        return re.sub(InterWikiMap._argspec_re, setarg, txt)

    def _expand_or_append(self, txt, args):
        """Like expand, but also append first arg if there's no "$"."""
        if not args:
            return txt
        expanded = self._expand(txt, args)
        return txt + args[0] if expanded == txt else expanded

    def url(self, ns, target):
        """Return `(url, title)` for the given InterWiki `ns`.

        Expand the colon-separated `target` arguments.
        """
        ns, url, title = self[ns]
        maxargnum = max(
            [0] +
            [int(a[1:]) for a in re.findall(InterWikiMap._argspec_re, url)])
        target, query, fragment = split_url_into_path_query_fragment(target)
        if maxargnum > 0:
            args = target.split(':', (maxargnum - 1))
        else:
            args = [target]
        url = self._expand_or_append(url, args)
        ntarget, nquery, nfragment = split_url_into_path_query_fragment(url)
        if query and nquery:
            nquery = '%s&%s' % (nquery, query[1:])
        else:
            nquery = nquery or query
        nfragment = fragment or nfragment  # user provided takes precedence
        expanded_url = ntarget + nquery + nfragment
        if not self._is_safe_url(expanded_url):
            expanded_url = ''
        expanded_title = self._expand(title, args)
        if expanded_title == title:
            expanded_title = _("%(target)s in %(name)s",
                               target=target,
                               name=title)
        return expanded_url, expanded_title

    # IWikiChangeListener methods

    def wiki_page_added(self, page):
        if page.name == InterWikiMap._page_name:
            del self.interwiki_map

    def wiki_page_changed(self, page, version, t, comment, author):
        if page.name == InterWikiMap._page_name:
            del self.interwiki_map

    def wiki_page_deleted(self, page):
        if page.name == InterWikiMap._page_name:
            del self.interwiki_map

    def wiki_page_version_deleted(self, page):
        if page.name == InterWikiMap._page_name:
            del self.interwiki_map

    @cached
    def interwiki_map(self):
        """Map from upper-cased namespaces to (namespace, prefix, title)
        values.
        """
        from trac.wiki.model import WikiPage
        map = {}
        content = WikiPage(self.env, InterWikiMap._page_name).text
        in_map = False
        for line in content.split('\n'):
            if in_map:
                if line.startswith('----'):
                    in_map = False
                else:
                    m = re.match(InterWikiMap._interwiki_re, line)
                    if m:
                        prefix, url, title = m.groups()
                        url = url.strip()
                        title = title.strip() if title else prefix
                        map[prefix.upper()] = (prefix, url, title)
            elif line.startswith('----'):
                in_map = True
        for prefix, value in self.interwiki_section.options():
            value = value.split(None, 1)
            if value:
                url = value[0].strip()
                title = value[1].strip() if len(value) > 1 else prefix
                map[prefix.upper()] = (prefix, url, title)
        return map

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'InterWiki'

    def get_macro_description(self, name):
        return 'messages', \
               N_("Provide a description list for the known InterWiki "
                  "prefixes.")

    def expand_macro(self, formatter, name, content):
        interwikis = []
        for k in sorted(self.keys()):
            prefix, url, title = self[k]
            interwikis.append({
                'prefix':
                prefix,
                'url':
                url,
                'title':
                title,
                'rc_url':
                self._expand_or_append(url, ['RecentChanges']),
                'description':
                url if title == prefix else title
            })

        return tag.table(tag.tr(tag.th(tag.em(
            _("Prefix"))), tag.th(tag.em(_("Site")))), [
                tag.tr(tag.td(tag.a(w['prefix'], href=w['rc_url'])),
                       tag.td(tag.a(w['description'], href=w['url'])))
                for w in interwikis
            ],
                         class_="wiki interwiki")

    # Internal methods

    def _is_safe_url(self, url):
        return WikiSystem(self.env).render_unsafe_content or \
               ':' not in url or \
               url.split(':', 1)[0] in self._safe_schemes

    @lazy
    def _safe_schemes(self):
        return set(WikiSystem(self.env).safe_schemes)
示例#10
0
class SubversionPropertyRenderer(Component):

    implements(IPropertyRenderer)

    svn_externals_section = ConfigSection(
        'svn:externals',
        """The TracBrowser for Subversion can interpret the `svn:externals`
        property of folders. By default, it only turns the URLs into links as
        Trac can't browse remote repositories.

        However, if you have another Trac instance (or an other repository
        browser like [http://www.viewvc.org/ ViewVC]) configured to browse the
        target repository, then you can instruct Trac which other repository
        browser to use for which external URL. This mapping is done in the
        `[svn:externals]` section of the TracIni.

        Example:
        {{{
        [svn:externals]
        1 = svn://server/repos1                       http://trac/proj1/browser/$path?rev=$rev
        2 = svn://server/repos2                       http://trac/proj2/browser/$path?rev=$rev
        3 = http://theirserver.org/svn/eng-soft       http://ourserver/viewvc/svn/$path/?pathrev=25914
        4 = svn://anotherserver.com/tools_repository  http://ourserver/tracs/tools/browser/$path?rev=$rev
        }}}
        With the above, the
        `svn://anotherserver.com/tools_repository/tags/1.1/tools` external will
        be mapped to `http://ourserver/tracs/tools/browser/tags/1.1/tools?rev=`
        (and `rev` will be set to the appropriate revision number if the
        external additionally specifies a revision, see the
        [%(svnbook)s SVN Book on externals] for more details).

        Note that the number used as a key in the above section is purely used
        as a place holder, as the URLs themselves can't be used as a key due to
        various limitations in the configuration file parser.

        Finally, the relative URLs introduced in
        [http://subversion.apache.org/docs/release-notes/1.5.html#externals Subversion 1.5]
        are not yet supported.
        """,
        doc_args={
            'svnbook':
            'http://svnbook.red-bean.com/en/1.7/svn.advanced.externals.html'
        })

    def __init__(self):
        self._externals_map = {}

    # IPropertyRenderer methods

    def match_property(self, name, mode):
        if name in ('svn:externals', 'svn:needs-lock'):
            return 4
        return 2 if name in ('svn:mergeinfo', 'svnmerge-blocked',
                             'svnmerge-integrated') else 0

    def render_property(self, name, mode, context, props):
        if name == 'svn:externals':
            return self._render_externals(props[name])
        elif name == 'svn:needs-lock':
            return self._render_needslock(context)
        elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'):
            return self._render_mergeinfo(name, mode, context, props)

    def _is_abs_url(self, url):
        return url and '://' in url

    def _render_externals(self, prop):
        if not self._externals_map:
            for dummykey, value in self.svn_externals_section.options():
                value = value.split()
                if len(value) != 2:
                    self.log.warning(
                        "svn:externals entry %s doesn't contain "
                        "a space-separated key value pair, "
                        "skipping.", dummykey)
                    continue
                key, value = value
                self._externals_map[key] = value.replace('%', '%%') \
                                           .replace('$path', '%(path)s') \
                                           .replace('$rev', '%(rev)s')
        externals = []
        for external in prop.splitlines():
            elements = external.split()
            if not elements:
                continue
            localpath, rev, url = elements[0], '', elements[-1]
            if localpath.startswith('#'):
                externals.append((external, None, None, None, None))
                continue
            if len(elements) == 3:
                rev = elements[1]
                rev = rev.replace('-r', '')
            # retrieve a matching entry in the externals map
            if not self._is_abs_url(url):
                externals.append((external, None, None, None, None))
                continue
            prefix = []
            base_url = url
            while base_url:
                if base_url in self._externals_map or base_url == u'/':
                    break
                base_url, pref = posixpath.split(base_url)
                prefix.append(pref)
            href = self._externals_map.get(base_url)
            revstr = ' at revision ' + rev if rev else ''
            if not href and (url.startswith('http://')
                             or url.startswith('https://')):
                href = url.replace('%', '%%')
            if href:
                remotepath = ''
                if prefix:
                    remotepath = posixpath.join(*reversed(prefix))
                externals.append(
                    (localpath, revstr, base_url, remotepath, href % {
                        'path': remotepath,
                        'rev': rev
                    }))
            else:
                externals.append((localpath, revstr, url, None, None))
        externals_data = []
        for localpath, rev, url, remotepath, href in externals:
            label = localpath
            if url is None:
                title = ''
            elif href:
                if url:
                    url = ' in ' + url
                label += rev + url
                title = ''.join((remotepath, rev, url))
            else:
                title = _('No svn:externals configured in trac.ini')
            externals_data.append((label, href, title))
        return tag.ul([
            tag.li(tag.a(label, href=href, title=title))
            for label, href, title in externals_data
        ])

    def _render_needslock(self, context):
        url = chrome_resource_path(context.req, 'common/lock-locked.png')
        return tag.img(src=url, alt=_("needs lock"), title=_("needs lock"))

    def _render_mergeinfo(self, name, mode, context, props):
        rows = []
        for row in props[name].splitlines():
            try:
                (path, revs) = row.rsplit(':', 1)
                rows.append(
                    [tag.td(path),
                     tag.td(revs.replace(',', u',\u200b'))])
            except ValueError:
                rows.append(tag.td(row, colspan=2))
        return tag.table(tag.tbody([tag.tr(row) for row in rows]),
                         class_='props')
示例#11
0
class Phrases(Component):
    """Highlight attentional phrases like `FIXME`.

    Phrases that are highlighted are defined in the `[wikiextras]` section in
    `trac.ini`. Use the `ShowPhrases` macro to show a list of currently defined
    phrases.
    """

    implements(IRequestFilter, ITemplateProvider, IWikiSyntaxProvider,
               IWikiMacroProvider)

    fixme_phrases = ListOption('wikiextras', 'fixme_phrases', 'BUG, FIXME',
                               doc=
        """A list of attentional phrases or single words, separated by comma
        (`,`) that will be highlighted to catch attention. Any delimiter
        `():<>` adjacent to a phrase will not be presented. (i.e. do not
        include any of these delimiters in this list). This makes it possible
        to naturally write, for example, `FIXME:` in a wiki text, but view the
        phrase highlighted without the colon (`:`) which would not look
        natural. Use the `ShowPhrases` macro to show a list of currently
        defined phrases.""")

    todo_phrases = ListOption('wikiextras', 'todo_phrases', 'REVIEW, TODO',
                              doc="Analogous to `FIXME`-phrases, but "
                                  "presentation is less eye-catching.")

    done_phrases = ListOption('wikiextras', 'done_phrases',
                              'DONE, DEBUGGED, FIXED, REVIEWED',
                              doc="Analogous to `FIXME`-phrases, but "
                                  "presentation is less eye-catching.")

    custom_phrases_section = ConfigSection('wikiextras-custom-phrases',
        """Custom phrases are configurable by providing associations
        between a CSS class and the list of phrases separated by comma.

        Example:
        {{{#!ini
        [wikiextras-custom-phrases]
        nice = NICE, COOL
        }}}
        """)

    def __init__(self):
        self.text = {}
        #noinspection PyArgumentList
        html_form = '<span class="wikiextras phrase %s">%s</span>'

        def add_style(style, phrases):
            for phrase in phrases:
                html = html_form % (style, phrase)
                self.text[phrase] = html
                for (d1, d2) in [(':', ':'), ('<', '>'), ('(', ')')]:
                    self.text['%s%s%s' % (d1, phrase, d2)] = html
                for d2 in [':']:
                    self.text['%s%s' % (phrase, d2)] = html

        for style, phrases in [('fixme', self.fixme_phrases),
                               ('todo', self.todo_phrases),
                               ('done', self.done_phrases)]:
            add_style(style, phrases)

        for style, phrases in self.custom_phrases_section.options():
            add_style(style, phrases.split(','))

    # IRequestFilter methods

    #noinspection PyUnusedLocal
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        add_stylesheet(req, 'wikiextras/css/phrases.css')
        return template, data, content_type

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return [('wikiextras', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return []

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        yield ('!?(?:%s)' % prepare_regexp(self.text), self._format_phrase)

    def get_link_resolvers(self):
        return []

    #noinspection PyUnusedLocal
    def _format_phrase(self, formatter, match, fullmatch):
        return Markup(self.text[match])

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'ShowPhrases'

    #noinspection PyUnusedLocal
    def get_macro_description(self, name):
        return cleandoc("""Renders in a table the list of known phrases that
                are highlighted to catch attention.

                Comment: Any delimiter `():<>` adjacent to a phrase will not be
                presented. This makes it possible to naturally write `FIXME:`,
                for example, but view the phrase highlighted without the colon
                (`:`) which would not look natural. Prefixing a phrase with `!`
                prevents it from being highlighted.
                """)

    #noinspection PyUnusedLocal
    def expand_macro(self, formatter, name, content, args=None):
        t = [render_table(p, '1',
                          lambda s: self._format_phrase(formatter, s, None))
             for p in [self.fixme_phrases, self.todo_phrases,
                       self.done_phrases]]
        style = 'border:none;text-align:center;vertical-align:top'
        spacer = tag.td(style='width:2em;border:none')
        return tag.table(tag.tr(tag.td(t[0], style=style), spacer,
                                tag.td(t[1], style=style), spacer,
                                tag.td(t[2], style=style)))
示例#12
0
class ConfigurableTicketWorkflow(Component):
    """Ticket action controller which provides actions according to a
    workflow defined in trac.ini.

    The workflow is defined in the `[ticket-workflow]` section of the
    [wiki:TracIni#ticket-workflow-section trac.ini] configuration file.
    """

    implements(IEnvironmentSetupParticipant, ITicketActionController)

    ticket_workflow_section = ConfigSection('ticket-workflow',
        """The workflow for tickets is controlled by plugins. By default,
        there's only a `ConfigurableTicketWorkflow` component in charge.
        That component allows the workflow to be configured via this section
        in the `trac.ini` file. See TracWorkflow for more details.

        (''since 0.11'')""")

    def __init__(self, *args, **kwargs):
        self.actions = self.get_all_actions()
        self.log.debug('Workflow actions at initialization: %s\n',
                       self.actions)

    # IEnvironmentSetupParticipant methods

    def environment_created(self):
        """When an environment is created, we provide the basic-workflow,
        unless a ticket-workflow section already exists.
        """
        if 'ticket-workflow' not in self.config.sections():
            load_workflow_config_snippet(self.config, 'basic-workflow.ini')
            self.config.save()
            self.actions = self.get_all_actions()

    def environment_needs_upgrade(self, db):
        """The environment needs an upgrade if there is no [ticket-workflow]
        section in the config.
        """
        return not list(self.config.options('ticket-workflow'))

    def upgrade_environment(self, db):
        """Insert a [ticket-workflow] section using the original-workflow"""
        load_workflow_config_snippet(self.config, 'original-workflow.ini')
        self.config.save()
        self.actions = self.get_all_actions()
        info_message = """

==== Upgrade Notice ====

The ticket Workflow is now configurable.

Your environment has been upgraded, but configured to use the original
workflow. It is recommended that you look at changing this configuration to use
basic-workflow.

Read TracWorkflow for more information (don't forget to 'wiki upgrade' as well)

"""
        self.log.info(info_message.replace('\n', ' ').replace('==', ''))
        print info_message

    # ITicketActionController methods

    def get_ticket_actions(self, req, ticket):
        """Returns a list of (weight, action) tuples that are valid for this
        request and this ticket."""
        # Get the list of actions that can be performed

        # Determine the current status of this ticket.  If this ticket is in
        # the process of being modified, we need to base our information on the
        # pre-modified state so that we don't try to do two (or more!) steps at
        # once and get really confused.
        status = ticket._old.get('status', ticket['status']) or 'new'

        ticket_perm = req.perm(ticket.resource)
        allowed_actions = []
        for action_name, action_info in self.actions.items():
            oldstates = action_info['oldstates']
            if oldstates == ['*'] or status in oldstates:
                # This action is valid in this state.  Check permissions.
                required_perms = action_info['permissions']
                if self._is_action_allowed(ticket_perm, required_perms):
                    allowed_actions.append((action_info['default'],
                                            action_name))
        # Append special `_reset` action if status is invalid.
        if status not in TicketSystem(self.env).get_all_status() + \
                         ['new', 'closed']:
            required_perms = self.actions['_reset'].get('permissions')
            if self._is_action_allowed(ticket_perm, required_perms):
                default = self.actions['_reset'].get('default')
                allowed_actions.append((default, '_reset'))
        return allowed_actions

    def _is_action_allowed(self, ticket_perm, required_perms):
        if not required_perms:
            return True
        for permission in required_perms:
            if permission in ticket_perm:
                return True
        return False

    def get_all_status(self):
        """Return a list of all states described by the configuration.

        """
        all_status = set()
        for attributes in self.actions.values():
            all_status.update(attributes['oldstates'])
            all_status.add(attributes['newstate'])
        all_status.discard('*')
        all_status.discard('')
        return all_status

    def render_ticket_action_control(self, req, ticket, action):

        self.log.debug('render_ticket_action_control: action "%s"', action)

        this_action = self.actions[action]
        status = this_action['newstate']
        operations = this_action['operations']
        current_owner = ticket._old.get('owner', ticket['owner'])
        author = get_reporter_id(req, 'author')
        format_author = partial(Chrome(self.env).format_author, req)
        formatted_current_owner = format_author(current_owner or _("(none)"))

        control = []  # default to nothing
        hints = []
        if 'reset_workflow' in operations:
            control.append(_("from invalid state"))
            hints.append(_("Current state no longer exists"))
        if 'del_owner' in operations:
            hints.append(_("The ticket will be disowned"))
        if 'set_owner' in operations:
            id = 'action_%s_reassign_owner' % action

            if 'set_owner' in this_action:
                owners = [x.strip() for x in
                          this_action['set_owner'].split(',')]
            elif self.config.getbool('ticket', 'restrict_owner'):
                perm = PermissionSystem(self.env)
                owners = perm.get_users_with_permission('TICKET_MODIFY')
                owners.sort()
            else:
                owners = None

            if owners is None:
                owner = req.args.get(id, author)
                control.append(tag_("to %(owner)s",
                                    owner=tag.input(type='text', id=id,
                                                    name=id, value=owner)))
                hints.append(_("The owner will be changed from "
                               "%(current_owner)s to the specified user",
                               current_owner=formatted_current_owner))
            elif len(owners) == 1:
                owner = tag.input(type='hidden', id=id, name=id,
                                  value=owners[0])
                formatted_new_owner = format_author(owners[0])
                control.append(tag_("to %(owner)s",
                                    owner=tag(formatted_new_owner, owner)))
                if ticket['owner'] != owners[0]:
                    hints.append(_("The owner will be changed from "
                                   "%(current_owner)s to %(selected_owner)s",
                                   current_owner=formatted_current_owner,
                                   selected_owner=formatted_new_owner))
            else:
                selected_owner = req.args.get(id, req.authname)
                control.append(tag_("to %(owner)s", owner=tag.select(
                    [tag.option(x, value=x,
                                selected=(x == selected_owner or None))
                     for x in owners],
                    id=id, name=id)))
                hints.append(_("The owner will be changed from "
                               "%(current_owner)s to the selected user",
                               current_owner=formatted_current_owner))
        elif 'set_owner_to_self' in operations and \
                ticket._old.get('owner', ticket['owner']) != author:
            hints.append(_("The owner will be changed from %(current_owner)s "
                           "to %(authname)s",
                           current_owner=formatted_current_owner,
                           authname=format_author(author)))
        if 'set_resolution' in operations:
            if 'set_resolution' in this_action:
                resolutions = [x.strip() for x in
                               this_action['set_resolution'].split(',')]
            else:
                resolutions = [r.name for r in Resolution.select(self.env)]
            if not resolutions:
                raise TracError(_("Your workflow attempts to set a resolution "
                                  "but none is defined (configuration issue, "
                                  "please contact your Trac admin)."))
            id = 'action_%s_resolve_resolution' % action
            if len(resolutions) == 1:
                resolution = tag.input(type='hidden', id=id, name=id,
                                       value=resolutions[0])
                control.append(tag_("as %(resolution)s",
                                    resolution=tag(resolutions[0],
                                                   resolution)))
                hints.append(_("The resolution will be set to %(name)s",
                               name=resolutions[0]))
            else:
                selected_option = req.args.get(id,
                        TicketSystem(self.env).default_resolution)
                control.append(tag_("as %(resolution)s",
                                    resolution=tag.select(
                    [tag.option(x, value=x,
                                selected=(x == selected_option or None))
                     for x in resolutions],
                    id=id, name=id)))
                hints.append(_("The resolution will be set"))
        if 'del_resolution' in operations:
            hints.append(_("The resolution will be deleted"))
        if 'leave_status' in operations:
            control.append(_("as %(status)s",
                             status= ticket._old.get('status',
                                                     ticket['status'])))
            if len(operations) == 1:
                hints.append(_("The owner will remain %(current_owner)s",
                               current_owner=formatted_current_owner)
                             if current_owner else
                             _("The ticket will remain with no owner"))
        else:
            if status != '*':
                hints.append(_("Next status will be '%(name)s'", name=status))
        return (this_action.get('name', action), tag(separated(control, ' ')),
                '. '.join(hints) + '.' if hints else '')

    def get_ticket_changes(self, req, ticket, action):
        this_action = self.actions[action]

        # Enforce permissions
        if not self._has_perms_for_action(req, this_action, ticket.resource):
            # The user does not have any of the listed permissions, so we won't
            # do anything.
            return {}

        updated = {}
        # Status changes
        status = this_action['newstate']
        if status != '*':
            updated['status'] = status

        for operation in this_action['operations']:
            if operation == 'del_owner':
                updated['owner'] = ''
            elif operation == 'set_owner':
                newowner = req.args.get('action_%s_reassign_owner' % action,
                                    this_action.get('set_owner', '').strip())
                # If there was already an owner, we get a list, [new, old],
                # but if there wasn't we just get new.
                if type(newowner) == list:
                    newowner = newowner[0]
                updated['owner'] = newowner
            elif operation == 'set_owner_to_self':
                updated['owner'] = get_reporter_id(req, 'author')
            elif operation == 'del_resolution':
                updated['resolution'] = ''
            elif operation == 'set_resolution':
                newresolution = req.args.get('action_%s_resolve_resolution' % \
                                             action,
                                this_action.get('set_resolution', '').strip())
                updated['resolution'] = newresolution

            # reset_workflow is just a no-op here, so we don't look for it.
            # leave_status is just a no-op here, so we don't look for it.
        return updated

    def apply_action_side_effects(self, req, ticket, action):
        pass

    def _has_perms_for_action(self, req, action, resource):
        required_perms = action['permissions']
        if required_perms:
            for permission in required_perms:
                if permission in req.perm(resource):
                    break
            else:
                # The user does not have any of the listed permissions
                return False
        return True

    # Public methods (for other ITicketActionControllers that want to use
    #                 our config file and provide an operation for an action)

    def get_all_actions(self):
        actions = parse_workflow_config(self.ticket_workflow_section.options())

        # Special action that gets enabled if the current status no longer
        # exists, as no other action can then change its state. (#5307/#11850)
        if '_reset' not in actions:
            reset = {
                'default': 0,
                'name': 'reset',
                'newstate': 'new',
                'oldstates': [],
                'operations': ['reset_workflow'],
                'permissions': ['TICKET_ADMIN']
            }
            for key, val in reset.items():
                actions['_reset'][key] = val

        for name, info in actions.iteritems():
            if not info['newstate']:
                self.log.warning("Ticket workflow action '%s' doesn't define "
                                 "any transitions", name)
        return actions

    def get_actions_by_operation(self, operation):
        """Return a list of all actions with a given operation
        (for use in the controller's get_all_status())
        """
        actions = [(info['default'], action) for action, info
                   in self.actions.items()
                   if operation in info['operations']]
        return actions

    def get_actions_by_operation_for_req(self, req, ticket, operation):
        """Return list of all actions with a given operation that are valid
        in the given state for the controller's get_ticket_actions().

        If state='*' (the default), all actions with the given operation are
        returned.
        """
        # Be sure to look at the original status.
        status = ticket._old.get('status', ticket['status'])
        actions = [(info['default'], action)
                   for action, info in self.actions.items()
                   if operation in info['operations'] and
                      ('*' in info['oldstates'] or
                       status in info['oldstates']) and
                      self._has_perms_for_action(req, info, ticket.resource)]
        return actions
示例#13
0
class WorkflowManager(Component):

    config_section = ConfigSection('ticket-workflow-action-buttons', '')

    @property
    def action_controllers(self):
        return TicketSystem(self.env).action_controllers

    def allowed_actions(self, allowed, req, ticket):
        return [
            action for action in TicketSystem(self.env).get_available_actions(
                req, ticket) if allowed is None or action in allowed
        ]

    def controllers_for_action(self, req, ticket, action):
        return [
            controller for controller in self.action_controllers if action in
            [i[1] for i in controller.get_ticket_actions(req, ticket)]
        ]

    def render_action_control(self, req, ticket, action):
        first_label = None
        widgets = []
        hints = []
        for controller in self.controllers_for_action(req, ticket, action):
            print controller, action
            label, widget, hint = controller.render_ticket_action_control(
                req, ticket, action)
            if first_label is None:
                first_label = label
            widgets.append(widget)
            hints.append(hint)
        return first_label, tag(*widgets), (hints and '. '.join(hints) or '')

    _default_icons = {
        "accept": "fa-thumbs-o-up",
        "leave": "fa-comments-o",
        "reassign": "fa-random",
        "reopen": "fa-minus-square-o",
        "resolve": "fa-check-square-o",
    }

    def render_action_button(self, req, ticket, action):
        template = """
              <label class="button" style="%(css)s">
                <input type="hidden" name="action" value="%(action)s" />
                <a %(comment_required)s name="act"><i class='fa %(icon)s'></i> %(title)s</a>
"""
        data = {
            "action":
            action,
            "css":
            self.config_section.get("%s.css" % action) or "",
            "comment_required":
            (self.config_section.get("%s.comment" % action) == "required"
             and 'data-comment="required"' or ""),
            "icon":
            self.config_section.get("%s.icon" % action,
                                    self._default_icons.get(action)),
            "title":
            self.config_section.get("%s.title" % action, action.title()),
        }
        markup = template % data

        supplemental_form = ""
        label, widgets, hints = self.render_action_control(req, ticket, action)
        if widgets.children:
            supplemental_form = "<div class='supplemental'><div class='supplemental-form'>%s %s <span class='hint'>%s</span><textarea style='width:95%%' rows='5' name='comment' placeholder='Enter your comment'></textarea><input type='submit' /></div></div>" % (
                action.title(), str(widgets), hints)
        markup = markup + supplemental_form + "</label>"
        return Markup(markup)
示例#14
0
class ConfigurableCommitTicketReferenceMacro(CommitTicketReferenceMacro):
    """
    An extension of Trac CommitTicketUpdater's CommitTicketReferenceMacro that
    does not search for occurrences of the ticket reference in a referenced
    commit's message. This avoids the dependency on CommitTicketUpdater.
    """

    # pylint: disable=abstract-method

    ticket_replace_section_name = 'commit-ticket-update-replace'
    ticket_replace_section = ConfigSection(
        ticket_replace_section_name,
        """In this section, you can define patterns for substitution in the
        commit message in the format:

        name.pattern = PR-\d+
        name.replace = https://example.org/$(repository)s/\1

        The name has no further meaning than identifying a pair of pattern and
        replace and will be ignored.

        The following variables will be substituted in both pattern and replace
        before applying the regular expression:

        - $(repository)s    name of the repository committed to
        - $(revision)s      revision of the commit

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

    def expand_macro(self, formatter, name, content, args=None):
        # pylint: disable=too-many-function-args
        args = args or {}
        reponame = args.get('repository') or ''
        rev = args.get('revision')
        # pylint: disable=no-member
        repos = RepositoryManager(self.env).get_repository(reponame)
        try:
            changeset = repos.get_changeset(rev)
            message = changeset.message
            rev = changeset.rev
            resource = repos.resource
        except Exception:  # pylint: disable=broad-except
            message = content
            resource = Resource('repository', reponame)
        config = self.ticket_replace_section
        fields = {}
        for key, value in config.options():
            idx = key.rfind('.')
            if idx >= 0:
                prefix, attribute = key[:idx], key[idx + 1:]
                field = fields.setdefault(prefix, {})
                field[attribute] = config.get(key)
            else:
                fields[key] = {'': value}
        for prefix, field in fields.iteritems():
            if not all(k in field for k in ['pattern', 'replace']):
                self.log.warn(
                    "Ignoring [%s] %s, missing .pattern or .replace" %
                    (self.ticket_replace_section_name, key))
                continue
            subst = {'repository': reponame, 'revision': rev}
            pattern = field['pattern'].replace('$(', '%(') % subst
            replace = field['replace'].replace('$(', '%(') % subst
            message = re.sub(pattern, replace, message)
        if ChangesetModule(self.env).wiki_format_messages:
            message = '\n'.join(
                map(lambda line: "> " + line, message.split('\n')))
            return tag.div(format_to_html(self.env,
                                          formatter.context.child(
                                              'changeset',
                                              rev,
                                              parent=resource),
                                          message,
                                          escape_newlines=True),
                           class_='message')
        else:
            return tag.pre(message, class_='message')
示例#15
0
class PasteParser(Component):

    implements(IRequestFilter, ITemplateProvider)

    paste_parser_section = ConfigSection('paste-parser',
        """ When text is pasted into the designated field of a new ticket
            this plugin can parse that pasted text and populate the results
            into other ticket fields as defined by these configuration options.
            This is useful where ticket information is received via email in a 
            very structured format that can be parsed. Users can copy-paste the email
            into one text field of a new ticket and then that pasted text will
            instantly be parsed and populated into the appropriate ticket fields for
            the user to review and adjust as needed before saving. 
        """)

    paste_parser_xref_section = ConfigSection('paste-parser-xref',
        """ These section contains all the information related to the fields that 
        should be searched for in the pasted text.
        """)

    pasted_text_pattern = Option('paste-parser', 'pasted_text_pattern',
        doc="""This is the regular expression pattern that this plugin will
               attempt to match against the text pasted into the designated field of a new 
               ticket. If this pattern does not find a match within the pasted text, then 
               nothing happens. If this pattern matches all or part of what was
               pasted into the designated field of a new ticket, then this plugin will immediately
               attempt to parse the matched text based on the the definitions in 
               this config and populate the resulting values into the related ticket fields.""")

    field_not_found_for_updating_label = Option('paste-parser', 'field_not_found_for_updating_label',
        doc="""Label inserted into the paste-field to identify fields that could not be found in the DOM to be updated.""")

    invalid_field_values_not_in_list_label = Option('paste-parser', 'invalid_field_values_not_in_list_label',
        doc="""Label inserted into the paste-field to identify field values that could not be assigned because the value is not in the defined SELECT list of OPTIONS.""")

    key_value_delimiter = Option('paste-parser', 'key_value_delimiter',
        doc="""This is the delimiter that will be appended to all given 'source_key' strings (the source line attribute label)
        when attempting to match field labels in the pasted designated field text.""")

    ignore_pattern = Option('paste-parser', 'ignore_pattern',
        doc="""This is the regular expression that defines which strings within the 
        pasted text should be ignored/skipped and not be parsed for key-values These strings
        are stripped out of the input string prior to processing.""")

    field_to_parse = Option('paste-parser', 'field_to_parse',
        doc="""This is the name of the ticket field to be parsed. To get the actual DOM element for this field,
        this field name will be converted to the ID of the field's DOM element via a regexp replace
        with the field_name_to_id_match as the match pattern and the field_name_to_id_replace as the
        replacement pattern.""")

    field_name_to_id_match = Option('paste-parser', 'field_name_to_id_match',
        doc="""This is a regexp matching pattern that will be used to match against the designated field name
        and then the field_name_to_id_replace will be used as the replacement pattern 
        to convert the field name to an ID so that we can get the actual DOM element for this field.""")

    field_name_to_id_replace = Option('paste-parser', 'field_name_to_id_replace',
        doc="""This is a regexp replacement pattern that will be used in conjunction with the field_name_to_id_match
        to convert the field name to an ID so that we can get the actual DOM element for this field.""")

    key_value_end_pattern = Option('paste-parser', 'key_value_end_pattern',
        doc="""This optional regexp matching pattern defines the end of each key/value pair. If not given, a new line 
        determines the end. This is useful if values span multiple lines.""")

    debug_on = Option('paste-parser', 'debug_on',
        doc="""If set to true, extensive debugging will be sent to the browser's console.""")


    def _get_xref(self):
        """Returns a list of dict describing the config options from trac.ini
           that define how to parse the pasted designated field text.
           Based on _get_ticket_groups() from Trac v1.2 trac/ticket/roadmap.py
        """
        if 'paste-parser-xref' in self.config:
            xrefs = {}
            order = 0
            for field_name, value in self.paste_parser_xref_section.options():
                qualifier = 'regexp' 
                if '.' in field_name:
                    field_name, qualifier = field_name.split('.', 1)
                self.log.debug("[PasteParser] field_name=%s qualifier=%s", field_name, qualifier)
                field = xrefs.setdefault(field_name, {'name': field_name,
                                                      'order': order})
                self.log.debug('[PasteParser] json.dumps(field) PRE='+json.dumps(field))
                field[qualifier] = value
                self.log.debug('[PasteParser] json.dumps(field) POST='+json.dumps(field))
                order = max(order, int(field['order'])) + 1
            self.log.debug('[PasteParser] json.dumps(xrefs)='+json.dumps(xrefs))
            return [field for field in sorted(xrefs.values(),
                                              key=lambda g: int(g['order']))]
        else:
            return None


    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if template == 'ticket.html' and req.path_info == '/newticket':
            add_script(req, 'PasteParser/js/PasteParser.js')
            
            paste_parser_config = { 
                'pasted_text_pattern':      self.pasted_text_pattern,
                'key_value_delimiter':      self.key_value_delimiter,
                'ignore_pattern':           self.ignore_pattern,
                'field_to_parse':           self.field_to_parse,
                'field_name_to_id_match':   self.field_name_to_id_match,
                'field_name_to_id_replace': self.field_name_to_id_replace,
                'key_value_end_pattern':    self.key_value_end_pattern,                
                'field_not_found_for_updating_label':    self.field_not_found_for_updating_label,                
                'invalid_field_values_not_in_list_label':    self.invalid_field_values_not_in_list_label,                
                'debug_on': self.debug_on,
                'xrefs': self._get_xref()
                }
            self.log.debug('[PasteParser] json.dumps(paste_parser_config)='+json.dumps(paste_parser_config));
            add_script_data(req, {SCRIPT_VARIABLE_NAME: paste_parser_config})

        return template, data, content_type

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('PasteParser', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return []
示例#16
0
class InterTracDispatcher(Component):
    """InterTrac dispatcher."""

    implements(IRequestHandler, IWikiMacroProvider)

    intertrac_section = ConfigSection(
        'intertrac',
        """This section configures InterTrac prefixes. Options in this section
        whose name contain a "." define aspects of the InterTrac prefix
        corresponding to the option name up to the ".". Options whose name
        don't contain a "." define an alias.
        
        The `.url` is mandatory and is used for locating the other Trac.
        This can be a relative URL in case that Trac environment is located
        on the same server.
        
        The `.title` information is used for providing a useful tooltip when
        moving the cursor over an InterTrac link.
        
        The `.compat` option can be used to activate or disable a
        ''compatibility'' mode:
         * If the targeted Trac is running a version below
           [trac:milestone:0.10 0.10] ([trac:r3526 r3526] to be precise), then
           it doesn't know how to dispatch an InterTrac link, and it's up to
           the local Trac to prepare the correct link. Not all links will work
           that way, but the most common do. This is called the compatibility
           mode, and is `true` by default.
         * If you know that the remote Trac knows how to dispatch InterTrac
           links, you can explicitly disable this compatibility mode and then
           ''any'' TracLinks can become InterTrac links.

        Example configuration:
        {{{
        [intertrac]
        # -- Example of setting up an alias:
        t = trac
        
        # -- Link to an external Trac:
        trac.title = Edgewall's Trac for Trac
        trac.url = http://trac.edgewall.org
        }}}
        """)

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'^/intertrac/(.*)', req.path_info)
        if match:
            if match.group(1):
                req.args['link'] = match.group(1)
            return True

    def process_request(self, req):
        link = req.args.get('link', '')
        parts = link.split(':', 1)
        if len(parts) > 1:
            resolver, target = parts
            if target[:1] + target[-1:] not in ('""', "''"):
                link = '%s:"%s"' % (resolver, target)
        from trac.web.chrome import web_context
        link_frag = extract_link(self.env, web_context(req), link)
        if isinstance(link_frag, (Element, Fragment)):
            elt = find_element(link_frag, 'href')
            if elt is None:  # most probably no permissions to view
                raise PermissionError(_("Can't view %(link)s:", link=link))
            href = elt.attrib.get('href')
        else:
            href = req.href(link.rstrip(':'))
        req.redirect(href)

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'InterTrac'

    def get_macro_description(self, name):
        return 'messages', N_("Provide a list of known InterTrac prefixes.")

    def expand_macro(self, formatter, name, content):
        intertracs = {}
        for key, value in self.intertrac_section.options():
            idx = key.rfind('.')
            if idx > 0:  # 0 itself doesn't help much: .xxx = ...
                prefix, attribute = key[:idx], key[idx + 1:]
                intertrac = intertracs.setdefault(prefix, {})
                intertrac[attribute] = value
            else:
                intertracs[key] = value  # alias
        if 'trac' not in intertracs:
            intertracs['trac'] = {
                'title': _('The Trac Project'),
                'url': 'http://trac.edgewall.org'
            }

        def generate_prefix(prefix):
            intertrac = intertracs[prefix]
            if isinstance(intertrac, basestring):
                yield tag.tr(tag.td(tag.b(prefix)),
                             tag.td('Alias for ', tag.b(intertrac)))
            else:
                url = intertrac.get('url', '')
                if url:
                    title = intertrac.get('title', url)
                    yield tag.tr(
                        tag.td(tag.a(tag.b(prefix), href=url + '/timeline')),
                        tag.td(tag.a(title, href=url)))

        return tag.table(class_="wiki intertrac")(
            tag.tr(tag.th(tag.em('Prefix')), tag.th(tag.em('Trac Site'))),
            [generate_prefix(p) for p in sorted(intertracs.keys())])
示例#17
0
class KeywordLabelsModule(Component):

    implements(IRequestFilter,
               ITemplateProvider,
               ITemplateStreamFilter)

    ticketlink_query = Option('query', 'ticketlink_query',
        default='?status=!closed')

    keyword_labels_section = ConfigSection('keyword-labels',
        """In this section, you can define custom label colors.""")

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        path = req.path_info
        if path.startswith('/ticket/') or path.startswith('/newticket'):
            if data and 'ticket' in data:
                ticket = data['ticket']
                keywords = ticket['keywords'] or ''
                for field in data.get('fields', ''):
                    if field.get('name') == 'keywords':
                        from trac.ticket.query import QueryModule
                        if not (isinstance(keywords, basestring) and
                                self.env.is_component_enabled(QueryModule)):
                            break
                        context = web_context(req, ticket)
                        field['rendered'] = self._query_link_words(context, 'keywords', keywords, 'keyword-label ticket')
        return template, data, content_type

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        yield 'keyword_labels', resource_filename(__name__, 'htdocs')

    def get_templates_dirs(self):
        return []

    # ITemplateStreamFilter methods

    def filter_stream(self, req, method, filename, stream, data):
        if req.path_info.startswith('/report') or req.path_info.startswith('/query'):
            from trac.ticket.query import QueryModule
            from trac.ticket.model import Ticket
            if not (self.env.is_component_enabled(QueryModule)) \
               and 'tickets' not in data \
               and 'row_groups' not in data:
                return stream

            reported_tickets = []
            if 'tickets' in data:
                class_ = 'keyword-label query'

                for row in data['tickets']:
                    try:
                        ticket = Ticket(self.env, row['id'])
                    except KeyError:
                        continue
                    else:
                        reported_tickets.insert(0, ticket)
            elif 'row_groups' in data:
                class_ = 'keyword-label report'

                for group in data['row_groups']:
                    for row in group[1]:
                        try:
                            ticket = Ticket(self.env, row['resource'].id)
                        except KeyError:
                            continue
                        else:
                            reported_tickets.insert(0, ticket)

            def find_change(stream):
                ticket = reported_tickets.pop()
                keywords = ticket['keywords'] or ''
                context = web_context(req, ticket)
                tag_ = self._query_link_words(context, 'keywords', keywords, class_, prepend=[tag.span(' ')])
                return itertools.chain(stream[0:5], tag_, stream[6:])

            xpath = '//table[@class="listing tickets"]/tbody/tr/td[@class="summary"]'
            stream |= Transformer(xpath).filter(find_change)

        add_stylesheet(req, 'keyword_labels/css/keyword_labels.css')
        return stream

    # Inner methods

    def _query_link_words(self, context, name, value, class_, prepend=None, append=None):
        """Splits a list of words and makes a query link to each separately"""
        from trac.ticket.query import QueryModule
        if not (isinstance(value, basestring) and  # None or other non-splitable
                self.env.is_component_enabled(QueryModule)):
            return value
        args = arg_list_to_args(parse_arg_list(self.ticketlink_query))
        items = []
        if prepend:
            items.extend(prepend)
        for i, word in enumerate(re.split(r'([;,\s]+)', value)):
            if i % 2:
                items.append(' ')
            elif word:
                backgroundColor = self.keyword_labels_section.get(word.lower())
                fontColor = self.keyword_labels_section.get(word.lower() + '.font_color', 'white')
                if not backgroundColor:
                    backgroundColor = ColorHash(word.encode('utf-8')).hex
                styles = {
                    'backgroundColor': backgroundColor,
                    'fontColor': fontColor,
                }
                word_args = args.copy()
                word_args[name] = '~' + word
                items.append(tag.a(word,
                                   style='background-color: {backgroundColor}; color: {fontColor}'.format(**styles),
                                   class_=class_,
                                   href=context.href.query(word_args)))
        if append:
            items.extend(append)
        return tag(items)
示例#18
0
class TracIniAdminPanel(Component):
    """ An editor panel for trac.ini. """

    implements(IAdminPanelProvider, ITemplateProvider)

    valid_section_name_chars = Option(
        'ini-editor',
        'valid-section-name-chars',
        '^[a-zA-Z0-9\\-_\\:]+$',
        doc="""Defines the valid characters for a section name or option name in 
      `trac.ini`. Must be a valid regular expression. You only need to change 
      these if you have plugins that use some strange section or option names.
      """,
        doc_domain="inieditorpanel")

    valid_option_name_chars = Option(
        'ini-editor',
        'valid-option-name-chars',
        '^[a-zA-Z0-9\\-_\\:.]+$',
        doc="""Defines the valid characters for a section name or option name in 
      `trac.ini`. Must be a valid regular expression. You only need to change 
      these if you have plugins that use some strange section or option names.
      """,
        doc_domain="inieditorpanel")

    security_manager = ExtensionOption(
        'ini-editor',
        'security-manager',
        IOptionSecurityManager,
        'IniEditorEmptySecurityManager',
        doc="""Defines the security manager that specifies whether the user has 
      access to certain options.
      """,
        doc_domain="inieditorpanel")

    # See "IniEditorBasicSecurityManager" for why we use a pipe char here.
    password_options = ListOption(
        'ini-editor',
        'password-options',
        doc="""Defines option fields (as `section-name|option-name`) that 
      represent passwords. Password input fields are used for these fields.
      Note the fields specified here are taken additionally to some predefined 
      fields provided by the ini editor.
      """,
        doc_domain="inieditorpanel")

    ini_section = ConfigSection(
        'ini-editor',
        """This section is used to handle configurations used by
      TracIniAdminPanel plugin.""",
        doc_domain='inieditorpanel')

    DEFAULT_PASSWORD_OPTIONS = {'notification|smtp_password': True}

    def __init__(self):
        """Set up translation domain"""
        locale_dir = resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)

        self.valid_section_name_chars_regexp = re.compile(
            self.valid_section_name_chars)
        self.valid_option_name_chars_regexp = re.compile(
            self.valid_option_name_chars)

        self.password_options_set = copy.deepcopy(
            self.DEFAULT_PASSWORD_OPTIONS)
        for option in self.password_options:
            self.password_options_set[option] = True

    #
    # IAdminPanelProvider methods
    #

    def get_admin_panels(self, req):
        if 'TRAC_ADMIN' in req.perm:
            yield ('general', dgettext('messages',
                                       'General'), 'trac_ini_editor',
                   _('trac.ini Editor'))

    def render_admin_panel(self, req, cat, page, path_info):
        req.perm.require('TRAC_ADMIN')

        if path_info == None:
            ext = ""
        else:
            ext = '/' + path_info

        #
        # Gather section names for section drop down field
        #
        all_section_names = []
        for section_name in self.config.sections():
            if section_name == 'components':
                continue
            all_section_names.append(section_name)

        # Check whether section exists and if it's not existing then check whether
        # its name is a valid section name.
        if (path_info is not None) and (path_info not in ('', '/', '_all_sections')) \
           and (path_info not in all_section_names):
            if path_info == 'components':
                add_warning(
                    req,
                    _('The section "components" can\'t be edited with the ini editor.'
                      ))
                req.redirect(req.href.admin(cat, page))
                return None
            elif self.valid_section_name_chars_regexp.match(path_info) is None:
                add_warning(req,
                            _('The section name %s is invalid.') % path_info)
                req.redirect(req.href.admin(cat, page))
                return None

            # Add current section if it's not already in the list. This happens if
            # the section is essentially empty (i.e. newly created with no non-default
            # option values and no option from the option registry).
            all_section_names.append(path_info)

        registry = ConfigSection.get_registry(self.compmgr)
        descriptions = {}
        for section_name, section in registry.items():
            if section_name == 'components':
                continue
            doc = section.__doc__
            if not section_name in all_section_names:
                all_section_names.append(section_name)
            if doc:
                descriptions[section_name] = dgettext(section.doc_domain, doc)

        all_section_names.sort()

        sections = {}

        #
        # Check security manager
        #
        manager = None
        try:
            manager = self.security_manager
        except Exception, detail:  # "except ... as ..." is only available since Python 2.6
            if req.method != 'POST':
                # only add this warning once
                add_warning(
                    req,
                    _('Security manager could not be initated. %s') %
                    unicode(detail))

        if manager is None:
            #
            # Security manager is not available
            #
            if req.method == 'POST':
                req.redirect(req.href.admin(cat, page) + ext)
                return None

        elif req.method == 'POST' and 'change-section' in req.args:
            #
            # Changing the section
            #
            req.redirect(
                req.href.admin(cat, page) + '/' + req.args['change-section'])
            return None

        elif req.method == 'POST' and 'new-section-name' in req.args:
            #
            # Create new section (essentially simply changing the section)
            #
            section_name = req.args['new-section-name'].strip()

            if section_name == '':
                add_warning(req, _('The section name was empty.'))
                req.redirect(req.href.admin(cat, page) + ext)
            elif section_name == 'components':
                add_warning(
                    req,
                    _('The section "components" can\'t be edited with the ini editor.'
                      ))
                req.redirect(req.href.admin(cat, page))
            elif self.valid_section_name_chars_regexp.match(
                    section_name) is None:
                add_warning(
                    req,
                    _('The section name %s is invalid.') % section_name)
                req.redirect(req.href.admin(cat, page) + ext)
            else:
                if section_name not in all_section_names:
                    add_notice(
                        req,
                        _('Section %s has been created. Note that you need to add at least one option to store it permanently.'
                          ) % section_name)
                else:
                    add_warning(req, _('The section already exists.'))
                req.redirect(req.href.admin(cat, page) + '/' + section_name)

            return None

        elif path_info is not None and path_info not in ('', '/'):
            #
            # Display and possibly modify section (if one is selected)
            #
            default_values = self.config.defaults()

            # Gather option values
            # NOTE: This needs to be done regardless whether we have POST data just to
            #   be on the safe site.
            if path_info == '_all_sections':
                # All sections
                custom_options = self._get_session_custom_options(req)
                # Only show sections with any data
                for section_name in all_section_names:
                    sections[section_name] = self._read_section_config(
                        req, section_name, default_values, custom_options)
            else:
                # Only a single section
                # Note: At this point path_info has already been verified to contain a
                #   valid section name (see check above).
                sections[path_info] = self._read_section_config(
                    req, path_info, default_values)

            #
            # Handle POST data
            #
            if req.method == 'POST':
                # Overwrite option values with POST values so that they don't get lost
                for key, value in req.args.items():
                    if not key.startswith(
                            'inieditor_value##'):  # skip unrelated args
                        continue

                    name = key[len('inieditor_value##'):].split('##')
                    section_name = name[0].strip()
                    option_name = name[1].strip()

                    if section_name == 'components':
                        continue

                    if option_name == 'dummy':
                        if section_name not in sections:
                            sections[section_name] = {}
                        continue

                    section = sections.get(section_name, None)
                    if section:
                        option = section.get(option_name, None)
                        if option:
                            self._set_option_value(req, section_name,
                                                   option_name, option, value)
                        else:
                            # option not available; was propably newly added
                            section[
                                option_name] = self._create_new_field_instance(
                                    req, section_name, option_name,
                                    default_values.get(section_name, None),
                                    value)
                    else:
                        # newly created section (not yet stored)
                        sections[section_name] = {
                            option_name:
                            self._create_new_field_instance(
                                req, section_name, option_name, None, value)
                        }

                # Check which options use their default values
                # NOTE: Must be done after assigning field value from the previous step
                #   to ensure that the default value has been initialized.
                if 'inieditor_default' in req.args:
                    default_using_options = req.args.get('inieditor_default')
                    if default_using_options is None or len(
                            default_using_options) == 0:
                        # if no checkbox was selected make this explicitly a list (just for safety)
                        default_using_options = []
                    elif type(default_using_options).__name__ != 'list':
                        # if there's only one checkbox it's just a string
                        default_using_options = [
                            unicode(default_using_options)
                        ]

                    for default_using_option in default_using_options:
                        name = default_using_option.split('##')
                        section_name = name[0].strip()
                        option_name = name[1].strip()
                        section = sections.get(section_name, None)
                        if section:
                            option = section.get(option_name, None)
                            if option:
                                if option['access'] == ACCESS_MODIFIABLE:
                                    option['value'] = option['default_value']
                            else:
                                # option not available; was propably newly added
                                section[
                                    option_name] = self._create_new_field_instance(
                                        req, section_name, option_name,
                                        default_values.get(section_name, None))
                        else:
                            # newly created section (not yet stored)
                            sections[section_name] = {
                                option_name:
                                self._create_new_field_instance(
                                    req, section_name, option_name, None)
                            }

                #
                # Identify submit type
                # NOTE: Using "cur_focused_field" is a hack to support hitting the
                #  return key even for the new-options field. Without this hitting
                #  return would always associated to the apply button.
                #
                submit_type = None
                cur_focused_field = req.args.get('inieditor_cur_focused_field',
                                                 '')
                if cur_focused_field.startswith('option-value-'):
                    submit_type = 'apply-' + cur_focused_field[
                        len('option-value-'):]
                elif cur_focused_field.startswith('new-options-'):
                    submit_type = 'addnewoptions-' + cur_focused_field[
                        len('new-options-'):]
                else:
                    for key in req.args:
                        if not key.startswith('inieditor-submit-'):
                            continue

                        submit_type = key[len('inieditor-submit-'):]
                        break

                if submit_type.startswith('apply'):  # apply changes
                    if submit_type.startswith('apply-'):
                        # apply only one section
                        section_name = submit_type[len('apply-'):].strip()
                        if self._apply_section_changes(req, section_name,
                                                       sections[section_name]):
                            add_notice(
                                req,
                                _('Changes for section %s have been applied.')
                                % section_name)
                            self.config.save()
                        else:
                            add_warning(req,
                                        _('No changes have been applied.'))
                    else:
                        # apply all sections
                        changes_applied = False
                        for section_name, options in sections.items():
                            if self._apply_section_changes(
                                    req, section_name, options):
                                changes_applied = True

                        if changes_applied:
                            add_notice(req, _('Changes have been applied.'))
                            self.config.save()
                        else:
                            add_warning(req,
                                        _('No changes have been applied.'))

                elif submit_type.startswith('discard'):
                    if submit_type.startswith('discard-'):
                        # discard only one section
                        section_name = submit_type[len('discard-'):].strip()
                        self._discard_section_changes(req, section_name,
                                                      sections[section_name])
                        add_notice(
                            req,
                            _('Your changes for section %s have been discarded.'
                              ) % section_name)
                    else:
                        # discard all sections
                        for section_name, options in sections.items():
                            self._discard_section_changes(
                                req, section_name, options)
                        add_notice(req, _('All changes have been discarded.'))

                elif submit_type.startswith('addnewoptions-'):
                    section_name = submit_type[len('addnewoptions-'):].strip()
                    section = sections[section_name]
                    new_option_names = req.args['new-options-' +
                                                section_name].split(',')
                    section_default_values = default_values.get(
                        section_name, None)

                    field_added = False
                    for new_option_name in new_option_names:
                        new_option_name = new_option_name.strip()
                        if new_option_name in section:
                            continue  # field already exists

                        if self.valid_option_name_chars_regexp.match(
                                new_option_name) is None:
                            add_warning(
                                req,
                                _('The option name %s is invalid.') %
                                new_option_name)
                            continue

                        new_option = self._create_new_field_instance(
                            req, section_name, new_option_name,
                            section_default_values)
                        if new_option['access'] != ACCESS_MODIFIABLE:
                            add_warning(
                                req,
                                _('The new option %s could not be added due to security restrictions.'
                                  ) % new_option_name)
                            continue

                        self._add_session_custom_option(
                            req, section_name, new_option_name)
                        field_added = True

                    if field_added:
                        add_notice(
                            req,
                            _('The new fields have been added to section %s.')
                            % section_name)
                    else:
                        add_warning(req, _('No new fields have been added.'))

                req.redirect(req.href.admin(cat, page) + ext)
                return None

        # Split sections dict for faster template rendering
        modifiable_options = {}
        readonly_options = {}
        hidden_options = {}
        for section_name, options in sections.items():
            sect_modifiable = {}
            sect_readonly = {}
            sect_hidden = {}
            for option_name, option in options.items():
                if option['access'] == ACCESS_MODIFIABLE:
                    sect_modifiable[option_name] = option
                elif option['access'] == ACCESS_READONLY:
                    sect_readonly[option_name] = option
                else:
                    sect_hidden[option_name] = option

            modifiable_options[section_name] = sect_modifiable
            readonly_options[section_name] = sect_readonly
            hidden_options[section_name] = sect_hidden

        registry = ConfigSection.get_registry(self.compmgr)
        descriptions = {}
        for name, section in registry.items():
            doc = section.__doc__
            if doc:
                descriptions[name] = dgettext(section.doc_domain, doc)

        data = {
            'all_section_names': all_section_names,
            'sections': sections,
            'descriptions': descriptions,
            'modifiable_options': modifiable_options,
            'readonly_options': readonly_options,
            'hidden_options': hidden_options
        }

        section_counters = {}
        settings_stored_values = {}
        for section_name, section in sections.iteritems():
            escaped = section_name.replace(':', '_')
            section_counters[escaped] = {'option_count': len(section)}
            settings_stored_values[escaped] = dict(
                (name, option['stored_value']) for name, option in
                modifiable_options[section_name].iteritems()
                if option['type'] != 'password')

        add_script_data(
            req, {
                'section_count':
                len(sections),
                'section_names':
                sorted(section_counters),
                'section_counters':
                section_counters,
                'settings_stored_values':
                settings_stored_values,
                'info_format':
                _("Modified: %(mod)d | Defaults: %(def)d | Options "
                  "count: %(opt)d"),
            })

        add_stylesheet(req, 'inieditorpanel/main.css')
        add_script(req, 'inieditorpanel/editor.js')
        return 'admin_tracini.html', data
示例#19
0
文件: api.py 项目: timgraham/trac
class NotificationSystem(Component):

    email_sender = ExtensionOption(
        'notification', 'email_sender', IEmailSender, 'SmtpEmailSender',
        """Name of the component implementing `IEmailSender`.

        This component is used by the notification system to send emails.
        Trac currently provides `SmtpEmailSender` for connecting to an SMTP
        server, and `SendmailEmailSender` for running a `sendmail`-compatible
        executable. (''since 0.12'')""")

    smtp_enabled = BoolOption('notification', 'smtp_enabled', 'false',
                              """Enable email notification.""")

    smtp_from = Option(
        'notification', 'smtp_from', 'trac@localhost',
        """Sender address to use in notification emails.

        At least one of `smtp_from` and `smtp_replyto` must be set, otherwise
        Trac refuses to send notification mails.""")

    smtp_from_name = Option('notification', 'smtp_from_name', '',
                            """Sender name to use in notification emails.""")

    smtp_from_author = BoolOption(
        'notification', 'smtp_from_author', 'false',
        """Use the author of the change as the sender in notification emails
           (e.g. reporter of a new ticket, author of a comment). If the
           author hasn't set an email address, `smtp_from` and
           `smtp_from_name` are used instead.
           (''since 1.0'')""")

    smtp_replyto = Option(
        'notification', 'smtp_replyto', 'trac@localhost',
        """Reply-To address to use in notification emails.

        At least one of `smtp_from` and `smtp_replyto` must be set, otherwise
        Trac refuses to send notification mails.""")

    smtp_always_cc_list = ListOption(
        'notification',
        'smtp_always_cc',
        '',
        sep=(',', ' '),
        doc="""Comma-separated list of email addresses to always send
               notifications to. Addresses can be seen by all recipients
               (Cc:).""")

    smtp_always_bcc_list = ListOption(
        'notification',
        'smtp_always_bcc',
        '',
        sep=(',', ' '),
        doc="""Comma-separated list of email addresses to always send
            notifications to. Addresses are not public (Bcc:).
            """)

    smtp_default_domain = Option(
        'notification', 'smtp_default_domain', '',
        """Default host/domain to append to addresses that do not specify
           one. Fully qualified addresses are not modified. The default
           domain is appended to all username/login for which an email
           address cannot be found in the user settings.""")

    ignore_domains_list = ListOption(
        'notification',
        'ignore_domains',
        '',
        doc="""Comma-separated list of domains that should not be considered
           part of email addresses (for usernames with Kerberos domains).""")

    admit_domains_list = ListOption(
        'notification',
        'admit_domains',
        '',
        doc="""Comma-separated list of domains that should be considered as
        valid for email addresses (such as localdomain).""")

    mime_encoding = Option(
        'notification', 'mime_encoding', 'none',
        """Specifies the MIME encoding scheme for emails.

        Supported values are: `none`, the default value which uses 7-bit
        encoding if the text is plain ASCII or 8-bit otherwise. `base64`,
        which works with any kind of content but may cause some issues with
        touchy anti-spam/anti-virus engine. `qp` or `quoted-printable`,
        which works best for european languages (more compact than base64) if
        8-bit encoding cannot be used.
        """)

    use_public_cc = BoolOption(
        'notification', 'use_public_cc', 'false',
        """Addresses in the To and Cc fields are visible to all recipients.

        If this option is disabled, recipients are put in the Bcc list.
        """)

    use_short_addr = BoolOption(
        'notification', 'use_short_addr', 'false',
        """Permit email address without a host/domain (i.e. username only).

        The SMTP server should accept those addresses, and either append
        a FQDN or use local delivery. See also `smtp_default_domain`. Do not
        use this option with a public SMTP server.
        """)

    smtp_subject_prefix = Option(
        'notification', 'smtp_subject_prefix', '__default__',
        """Text to prepend to subject line of notification emails.

        If the setting is not defined, then `[$project_name]` is used as the
        prefix. If no prefix is desired, then specifying an empty option
        will disable it.
        """)

    notification_subscriber_section = ConfigSection(
        'notification-subscriber',
        """The notifications subscriptions are controlled by plugins. All
        `INotificationSubscriber` components are in charge. These components
        may allow to be configured via this section in the `trac.ini` file.

        See TracNotification for more details.

        Available subscribers:
        [[SubscriberList]]
        """)

    distributors = ExtensionPoint(INotificationDistributor)
    subscribers = ExtensionPoint(INotificationSubscriber)

    @property
    def smtp_always_cc(self):  # For backward compatibility
        return self.config.get('notification', 'smtp_always_cc')

    @property
    def smtp_always_bcc(self):  # For backward compatibility
        return self.config.get('notification', 'smtp_always_bcc')

    @property
    def ignore_domains(self):  # For backward compatibility
        return self.config.get('notification', 'ignore_domains')

    @property
    def admit_domains(self):  # For backward compatibility
        return self.config.get('notification', 'admit_domains')

    @lazy
    def subscriber_defaults(self):
        rawsubscriptions = self.notification_subscriber_section.options()
        return parse_subscriber_config(rawsubscriptions)

    def default_subscriptions(self, klass):
        for d in self.subscriber_defaults[klass]:
            yield (klass, d['distributor'], d['format'], d['priority'],
                   d['adverb'])

    def send_email(self, from_addr, recipients, message):
        """Send message to recipients via e-mail."""
        self.email_sender.send(from_addr, recipients, message)

    def notify(self, event):
        """Distribute an event to all subscriptions.

        :param event: a `NotificationEvent`
        """
        self.distribute_event(event, self.subscriptions(event))

    def distribute_event(self, event, subscriptions):
        """Distribute a event to all subscriptions.

        :param event: a `NotificationEvent`
        :param subscriptions: a list of tuples (sid, authenticated, address,
                              transport, format) where either sid or
                              address can be `None`
        """
        packages = {}
        for sid, authenticated, address, transport, format in subscriptions:
            package = packages.setdefault(transport, set())
            package.add((sid, authenticated, address, format))
        for distributor in self.distributors:
            for transport in distributor.transports():
                if transport in packages:
                    recipients = list(packages[transport])
                    distributor.distribute(transport, recipients, event)

    def subscriptions(self, event):
        """Return all subscriptions for a given event.

        :return: a list of (sid, authenticated, address, transport, format)
        """
        subscriptions = []
        for subscriber in self.subscribers:
            subscriptions.extend(x for x in subscriber.matches(event) if x)

        # For each (transport, sid, authenticated) combination check the
        # subscription with the highest priority:
        # If it is "always" keep it. If it is "never" drop it.

        # sort by (transport, sid, authenticated, priority)
        ordered = sorted(subscriptions, key=itemgetter(1, 2, 3, 6))
        previous_combination = None
        for rule, transport, sid, auth, addr, fmt, prio, adverb in ordered:
            if (transport, sid, auth) == previous_combination:
                continue
            if adverb == 'always':
                self.log.debug(
                    "Adding (%s [%s]) for 'always' on rule (%s) "
                    "for (%s)", sid, auth, rule, transport)
                yield (sid, auth, addr, transport, fmt)
            else:
                self.log.debug(
                    "Ignoring (%s [%s]) for 'never' on rule (%s) "
                    "for (%s)", sid, auth, rule, transport)
            # Also keep subscriptions without sid (raw email subscription)
            if sid:
                previous_combination = (transport, sid, auth)
示例#20
0
    def render_admin_panel(self, req, cat, page, path_info):
        req.perm.require('TRAC_ADMIN')

        if path_info == None:
            ext = ""
        else:
            ext = '/' + path_info

        #
        # Gather section names for section drop down field
        #
        all_section_names = []
        for section_name in self.config.sections():
            if section_name == 'components':
                continue
            all_section_names.append(section_name)

        # Check whether section exists and if it's not existing then check whether
        # its name is a valid section name.
        if (path_info is not None) and (path_info not in ('', '/', '_all_sections')) \
           and (path_info not in all_section_names):
            if path_info == 'components':
                add_warning(
                    req,
                    _('The section "components" can\'t be edited with the ini editor.'
                      ))
                req.redirect(req.href.admin(cat, page))
                return None
            elif self.valid_section_name_chars_regexp.match(path_info) is None:
                add_warning(req,
                            _('The section name %s is invalid.') % path_info)
                req.redirect(req.href.admin(cat, page))
                return None

            # Add current section if it's not already in the list. This happens if
            # the section is essentially empty (i.e. newly created with no non-default
            # option values and no option from the option registry).
            all_section_names.append(path_info)

        registry = ConfigSection.get_registry(self.compmgr)
        descriptions = {}
        for section_name, section in registry.items():
            if section_name == 'components':
                continue
            doc = section.__doc__
            if not section_name in all_section_names:
                all_section_names.append(section_name)
            if doc:
                descriptions[section_name] = dgettext(section.doc_domain, doc)

        all_section_names.sort()

        sections = {}

        #
        # Check security manager
        #
        manager = None
        try:
            manager = self.security_manager
        except Exception, detail:  # "except ... as ..." is only available since Python 2.6
            if req.method != 'POST':
                # only add this warning once
                add_warning(
                    req,
                    _('Security manager could not be initated. %s') %
                    unicode(detail))
示例#21
0
class ConfigurableTicketWorkflow(Component):
    """Ticket action controller which provides actions according to a
    workflow defined in trac.ini.

    The workflow is defined in the `[ticket-workflow]` section of the
    [wiki:TracIni#ticket-workflow-section trac.ini] configuration file.
    """

    implements(IEnvironmentSetupParticipant, ITicketActionController)

    ticket_workflow_section = ConfigSection('ticket-workflow',
        """The workflow for tickets is controlled by plugins. By default,
        there's only a `ConfigurableTicketWorkflow` component in charge.
        That component allows the workflow to be configured via this section
        in the `trac.ini` file. See TracWorkflow for more details.
        """)

    operations = ('del_owner', 'set_owner', 'set_owner_to_self',
                  'may_set_owner', 'set_resolution', 'del_resolution',
                  'leave_status', 'reset_workflow')

    def __init__(self):
        self.actions = self.get_all_actions()
        self.log.debug('Workflow actions at initialization: %s\n',
                       self.actions)

    # IEnvironmentSetupParticipant methods

    def environment_created(self):
        """When an environment is created, we provide the basic-workflow,
        unless a ticket-workflow section already exists.
        """
        if 'ticket-workflow' not in self.config.sections():
            load_workflow_config_snippet(self.config, 'basic-workflow.ini')
            self.config.save()
            self.actions = self.get_all_actions()

    def environment_needs_upgrade(self):
        pass

    def upgrade_environment(self):
        pass

    # ITicketActionController methods

    def get_ticket_actions(self, req, ticket):
        """Returns a list of (weight, action) tuples that are valid for this
        request and this ticket."""
        # Get the list of actions that can be performed

        # Determine the current status of this ticket.  If this ticket is in
        # the process of being modified, we need to base our information on the
        # pre-modified state so that we don't try to do two (or more!) steps at
        # once and get really confused.
        ticket_status = ticket._old.get('status', ticket['status'])
        exists = ticket_status is not None
        ticket_owner = ticket._old.get('owner', ticket['owner'])
        author = get_reporter_id(req, 'author')

        resource = ticket.resource
        allowed_actions = []
        for action_name, action_info in self.actions.items():
            operations = action_info['operations']
            newstate = action_info['newstate']
            # Exclude action that is effectively a No-op.
            if len(operations) == 1 and \
                    operations[0] == 'set_owner_to_self' and \
                    ticket_owner == author and ticket_status == newstate:
                continue
            if operations and not \
                    any(opt in self.operations for opt in operations):
                continue  # Ignore operations not defined by this controller
            oldstates = action_info['oldstates']
            if exists and oldstates == ['*'] or ticket_status in oldstates:
                # This action is valid in this state.  Check permissions.
                if self._is_action_allowed(req, action_info, resource):
                    allowed_actions.append((action_info['default'],
                                            action_name))
        # Append special `_reset` action if status is invalid.
        if exists and '_reset' in self.actions and \
                ticket_status not in TicketSystem(self.env).get_all_status():
            reset = self.actions['_reset']
            if self._is_action_allowed(req, reset, resource):
                allowed_actions.append((reset['default'], '_reset'))
        return allowed_actions

    def _is_action_allowed(self, req, action, resource):
        """Returns `True` if the workflow action is allowed for the `resource`.
        """
        perm_cache = req.perm(resource)
        required_perms = action['permissions']
        if required_perms:
            for permission in required_perms:
                if permission in perm_cache:
                    break
            else:
                return False
        return True

    def get_all_status(self):
        """Return a list of all states described by the configuration.

        """
        all_status = set()
        for attributes in self.actions.values():
            all_status.update(attributes['oldstates'])
            all_status.add(attributes['newstate'])
        all_status.discard('*')
        all_status.discard('')
        all_status.discard(None)
        return all_status

    def render_ticket_action_control(self, req, ticket, action):

        self.log.debug('render_ticket_action_control: action "%s"', action)

        this_action = self.actions[action]
        status = this_action['newstate']
        operations = this_action['operations']
        ticket_owner = ticket._old.get('owner', ticket['owner'])
        ticket_status = ticket._old.get('status', ticket['status'])
        author = get_reporter_id(req, 'author')
        author_info = partial(Chrome(self.env).authorinfo, req,
                              resource=ticket.resource)
        format_author = partial(Chrome(self.env).format_author, req,
                                resource=ticket.resource)
        formatted_current_owner = author_info(ticket_owner)
        exists = ticket_status is not None

        ticket_system = TicketSystem(self.env)
        control = []  # default to nothing
        hints = []
        if 'reset_workflow' in operations:
            control.append(_("from invalid state"))
            hints.append(_("Current state no longer exists"))
        if 'del_owner' in operations:
            hints.append(_("The ticket will be disowned"))
        if 'set_owner' in operations or 'may_set_owner' in operations:
            owners = self.get_allowed_owners(req, ticket, this_action)

            if 'set_owner' in operations:
                default_owner = author
            elif 'may_set_owner' in operations:
                if not exists:
                    default_owner = ticket_system.default_owner
                else:
                    default_owner = ticket_owner or None
                if owners is not None and default_owner not in owners:
                    owners.insert(0, default_owner)
            else:
                # Protect against future modification for case that another
                # operation is added to the outer conditional
                raise AssertionError(operations)

            id = 'action_%s_reassign_owner' % action

            if not owners:
                owner = req.args.get(id, default_owner)
                control.append(
                    tag_("to %(owner)s",
                         owner=tag.input(type='text', id=id, name=id,
                                         value=owner)))
                if not exists or ticket_owner is None:
                    hints.append(_("The owner will be the specified user"))
                else:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to the specified "
                                      "user",
                                      current_owner=formatted_current_owner))
            elif len(owners) == 1:
                owner = tag.input(type='hidden', id=id, name=id,
                                  value=owners[0])
                formatted_new_owner = author_info(owners[0])
                control.append(tag_("to %(owner)s",
                                    owner=tag(formatted_new_owner, owner)))
                if not exists or ticket_owner is None:
                    hints.append(tag_("The owner will be %(new_owner)s",
                                      new_owner=formatted_new_owner))
                elif ticket['owner'] != owners[0]:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to %(new_owner)s",
                                      current_owner=formatted_current_owner,
                                      new_owner=formatted_new_owner))
            else:
                selected_owner = req.args.get(id, default_owner)
                control.append(tag_("to %(owner)s", owner=tag.select(
                    [tag.option(label, value=value if value is not None else '',
                                selected=(value == selected_owner or None))
                     for label, value in sorted((format_author(owner), owner)
                                                for owner in owners)],
                    id=id, name=id)))
                if not exists or ticket_owner is None:
                    hints.append(_("The owner will be the selected user"))
                else:
                    hints.append(tag_("The owner will be changed from "
                                      "%(current_owner)s to the selected user",
                                      current_owner=formatted_current_owner))
        elif 'set_owner_to_self' in operations:
            formatted_author = author_info(author)
            if not exists or ticket_owner is None:
                hints.append(tag_("The owner will be %(new_owner)s",
                                  new_owner=formatted_author))
            elif ticket_owner != author:
                hints.append(tag_("The owner will be changed from "
                                  "%(current_owner)s to %(new_owner)s",
                                  current_owner=formatted_current_owner,
                                  new_owner=formatted_author))
            elif ticket_status != status:
                hints.append(tag_("The owner will remain %(current_owner)s",
                                  current_owner=formatted_current_owner))
        if 'set_resolution' in operations:
            resolutions = [r.name for r in Resolution.select(self.env)]
            if 'set_resolution' in this_action:
                valid_resolutions = set(resolutions)
                resolutions = this_action['set_resolution']
                if any(x not in valid_resolutions for x in resolutions):
                    raise ConfigurationError(_(
                        "Your workflow attempts to set a resolution but uses "
                        "undefined resolutions (configuration issue, please "
                        "contact your Trac admin)."))
            if not resolutions:
                raise ConfigurationError(_(
                    "Your workflow attempts to set a resolution but none is "
                    "defined (configuration issue, please contact your Trac "
                    "admin)."))
            id = 'action_%s_resolve_resolution' % action
            if len(resolutions) == 1:
                resolution = tag.input(type='hidden', id=id, name=id,
                                       value=resolutions[0])
                control.append(tag_("as %(resolution)s",
                                    resolution=tag(resolutions[0],
                                                   resolution)))
                hints.append(tag_("The resolution will be set to %(name)s",
                                  name=resolutions[0]))
            else:
                selected_option = req.args.get(id,
                                               ticket_system.default_resolution)
                control.append(tag_("as %(resolution)s",
                                    resolution=tag.select(
                    [tag.option(x, value=x,
                                selected=(x == selected_option or None))
                     for x in resolutions],
                    id=id, name=id)))
                hints.append(_("The resolution will be set"))
        if 'del_resolution' in operations:
            hints.append(_("The resolution will be deleted"))
        if 'leave_status' in operations:
            control.append(tag_("as %(status)s", status=ticket_status))
            if len(operations) == 1:
                hints.append(tag_("The owner will remain %(current_owner)s",
                                  current_owner=formatted_current_owner)
                             if ticket_owner else
                             _("The ticket will remain with no owner"))
        elif not operations:
            if status != '*':
                if ticket['status'] is None:
                    hints.append(tag_("The status will be '%(name)s'",
                                      name=status))
                else:
                    hints.append(tag_("Next status will be '%(name)s'",
                                      name=status))
        return (this_action['label'], tag(separated(control, ' ')),
                tag(separated(hints, '. ', '.') if hints else ''))

    def get_ticket_changes(self, req, ticket, action):
        this_action = self.actions[action]

        # Enforce permissions
        if not self._is_action_allowed(req, this_action, ticket.resource):
            # The user does not have any of the listed permissions, so we won't
            # do anything.
            return {}

        updated = {}
        # Status changes
        status = this_action['newstate']
        if status != '*':
            updated['status'] = status

        for operation in this_action['operations']:
            if operation == 'del_owner':
                updated['owner'] = ''
            elif operation in ('set_owner', 'may_set_owner'):
                set_owner = this_action.get('set_owner')
                newowner = req.args.get('action_%s_reassign_owner' % action,
                                        set_owner[0] if set_owner else '')
                # If there was already an owner, we get a list, [new, old],
                # but if there wasn't we just get new.
                if type(newowner) == list:
                    newowner = newowner[0]
                updated['owner'] = self._sub_owner_keyword(newowner, ticket)
            elif operation == 'set_owner_to_self':
                updated['owner'] = get_reporter_id(req, 'author')
            elif operation == 'del_resolution':
                updated['resolution'] = ''
            elif operation == 'set_resolution':
                set_resolution = this_action.get('set_resolution')
                newresolution = req.args.get('action_%s_resolve_resolution'
                                             % action,
                                             set_resolution[0]
                                             if set_resolution else '')
                updated['resolution'] = newresolution

            # reset_workflow is just a no-op here, so we don't look for it.
            # leave_status is just a no-op here, so we don't look for it.

        # Set owner to component owner for 'new' ticket if:
        #  - ticket doesn't exist and owner is < default >
        #  - component is changed
        #  - owner isn't explicitly changed
        #  - ticket owner is equal to owner of previous component
        #  - new component has an owner
        if not ticket.exists and 'owner' not in updated:
            updated['owner'] = self._sub_owner_keyword(ticket['owner'], ticket)
        elif ticket['status'] == 'new' and \
                'component' in ticket.values and \
                'component' in ticket._old and \
                'owner' not in updated:
            try:
                old_comp = TicketComponent(self.env, ticket._old['component'])
            except ResourceNotFound:
                # If the old component has been removed from the database
                # we just leave the owner as is.
                pass
            else:
                old_owner = old_comp.owner or ''
                current_owner = ticket['owner'] or ''
                if old_owner == current_owner:
                    new_comp = TicketComponent(self.env, ticket['component'])
                    if new_comp.owner:
                        updated['owner'] = new_comp.owner

        return updated

    def apply_action_side_effects(self, req, ticket, action):
        pass

    # Public methods (for other ITicketActionControllers that want to use
    #                 our config file and provide an operation for an action)

    def get_all_actions(self):
        actions = parse_workflow_config(self.ticket_workflow_section.options())

        has_new_state = any('new' in [a['newstate']] + a['oldstates']
                            for a in actions.itervalues())
        if has_new_state:
            # Special action that gets enabled if the current status no
            # longer exists, as no other action can then change its state.
            # (#5307/#11850)
            reset = {
                'default': 0,
                'label': 'Reset',
                'newstate': 'new',
                'oldstates': [],
                'operations': ['reset_workflow'],
                'permissions': ['TICKET_ADMIN']
            }
            for key, val in reset.items():
                actions['_reset'].setdefault(key, val)

        for name, info in actions.iteritems():
            for val in ('<none>', '< none >'):
                sub_val(actions[name]['oldstates'], val, None)
            if not info['newstate']:
                self.log.warning("Ticket workflow action '%s' doesn't define "
                                 "any transitions", name)
        return actions

    def get_actions_by_operation(self, operation):
        """Return a list of all actions with a given operation
        (for use in the controller's get_all_status())
        """
        actions = [(info['default'], action) for action, info
                   in self.actions.items()
                   if operation in info['operations']]
        return actions

    def get_actions_by_operation_for_req(self, req, ticket, operation):
        """Return list of all actions with a given operation that are valid
        in the given state for the controller's get_ticket_actions().

        If state='*' (the default), all actions with the given operation are
        returned.
        """
        # Be sure to look at the original status.
        status = ticket._old.get('status', ticket['status'])
        actions = [(info['default'], action)
                   for action, info in self.actions.items()
                   if operation in info['operations'] and
                      ('*' in info['oldstates'] or
                       status in info['oldstates']) and
                      self._is_action_allowed(req, info, ticket.resource)]
        return actions

    # Public methods

    def get_allowed_owners(self, req, ticket, action):
        """Returns users listed in the `set_owner` field of the action or
        possessing the `TICKET_MODIFY` permission if `set_owner` is not
        specified.

        This method can be overridden in a subclasses in order to
        customize the list of users that populate the assign-to select
        box.

        :since: 1.3.2
        """
        if 'set_owner' in action:
            return self._to_users(action['set_owner'], ticket)
        elif TicketSystem(self.env).restrict_owner:
            users = PermissionSystem(self.env)\
                    .get_users_with_permission('TICKET_MODIFY')
            cache = partial(PermissionCache, self.env, resource=ticket.resource)
            return sorted(u for u in users
                            if 'TICKET_MODIFY' in cache(username=u))

    # Internal methods

    def _sub_owner_keyword(self, owner, ticket):
        """Substitute keywords from the default_owner field.

        < default > -> component owner
        """
        if owner in ('< default >', '<default>'):
            default_owner = ''
            if ticket['component']:
                try:
                    component = TicketComponent(self.env, ticket['component'])
                except ResourceNotFound:
                    pass  # No such component exists
                else:
                    default_owner = component.owner  # May be empty
            return default_owner
        return owner

    def _to_users(self, users_perms_and_groups, ticket):
        """Finds all users contained in the list of `users_perms_and_groups`
        by recursive lookup of users when a `group` is encountered.
        """
        ps = PermissionSystem(self.env)
        groups = ps.get_groups_dict()

        def append_owners(users_perms_and_groups):
            for user_perm_or_group in users_perms_and_groups:
                if user_perm_or_group == 'authenticated':
                    owners.update({u[0] for u in self.env.get_known_users()})
                elif user_perm_or_group.isupper():
                    perm = user_perm_or_group
                    for user in ps.get_users_with_permission(perm):
                        if perm in PermissionCache(self.env, user,
                                                   ticket.resource):
                            owners.add(user)
                elif user_perm_or_group not in groups:
                    owners.add(user_perm_or_group)
                else:
                    append_owners(groups[user_perm_or_group])

        owners = set()
        append_owners(users_perms_and_groups)

        return sorted(owners)
示例#22
0
class PygmentsRenderer(Component):
    """HTML renderer for syntax highlighting based on Pygments."""

    implements(ISystemInfoProvider, IHTMLPreviewRenderer,
               IPreferencePanelProvider, IRequestHandler, ITemplateProvider)

    is_valid_default_handler = False

    pygments_lexer_options = ConfigSection(
        'pygments-lexer',
        """Configure Pygments [%(url)s lexer] options.

        For example, to set the
        [%(url)s#lexers-for-php-and-related-languages PhpLexer] options
        `startinline` and `funcnamehighlighting`:
        {{{#!ini
        [pygments-lexer]
        php.startinline = True
        php.funcnamehighlighting = True
        }}}

        The lexer name is derived from the class name, with `Lexer` stripped
        from the end. The lexer //short names// can also be used in place
        of the lexer name.
        """,
        doc_args={'url': 'http://pygments.org/docs/lexers/'})

    default_style = Option(
        'mimeviewer', 'pygments_default_style', 'trac',
        """The default style to use for Pygments syntax highlighting.""")

    pygments_modes = ListOption(
        'mimeviewer',
        'pygments_modes',
        '',
        doc="""List of additional MIME types known by Pygments.

        For each, a tuple `mimetype:mode:quality` has to be
        specified, where `mimetype` is the MIME type,
        `mode` is the corresponding Pygments mode to be used
        for the conversion and `quality` is the quality ratio
        associated to this conversion. That can also be used
        to override the default quality ratio used by the
        Pygments render.""")

    expand_tabs = True
    returns_source = True

    QUALITY_RATIO = 7

    EXAMPLE = """<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello, world!</title>
    <script>
      jQuery(function($) {
        $("h1").fadeIn("slow");
      });
    </script>
  </head>
  <body>
    <h1>Hello, world!</h1>
  </body>
</html>"""

    # ISystemInfoProvider methods

    def get_system_info(self):
        yield 'Pygments', get_pkginfo(pygments).get('version')

    # IHTMLPreviewRenderer methods

    def get_extra_mimetypes(self):
        for _, aliases, _, mimetypes in get_all_lexers():
            for mimetype in mimetypes:
                yield mimetype, aliases

    def get_quality_ratio(self, mimetype):
        # Extend default MIME type to mode mappings with configured ones
        try:
            return self._types[mimetype][1]
        except KeyError:
            return 0

    def render(self, context, mimetype, content, filename=None, rev=None):
        req = context.req
        style = req.session.get('pygments_style', self.default_style)
        add_stylesheet(req, '/pygments/%s.css' % style)
        try:
            if len(content) > 0:
                mimetype = mimetype.split(';', 1)[0]
                language = self._types[mimetype][0]
                return self._generate(language, content, context)
        except (KeyError, ValueError):
            raise Exception("No Pygments lexer found for mime-type '%s'." %
                            mimetype)

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        yield 'pygments', _('Syntax Highlighting')

    def render_preference_panel(self, req, panel):
        styles = list(get_all_styles())

        if req.method == 'POST':
            style = req.args.get('style')
            if style and style in styles:
                req.session['pygments_style'] = style
                add_notice(req, _("Your preferences have been saved."))
            req.redirect(req.href.prefs(panel or None))

        for style in sorted(styles):
            add_stylesheet(req,
                           '/pygments/%s.css' % style,
                           title=style.title())
        output = self._generate('html', self.EXAMPLE)
        return 'prefs_pygments.html', {
            'output': output,
            'selection': req.session.get('pygments_style', self.default_style),
            'styles': styles
        }

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/pygments/([-\w]+)\.css', req.path_info)
        if match:
            req.args['style'] = match.group(1)
            return True

    def process_request(self, req):
        style = req.args['style']
        try:
            style_cls = get_style_by_name(style)
        except ValueError as e:
            raise HTTPNotFound(e)

        parts = style_cls.__module__.split('.')
        filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py')
        mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz)
        last_modified = http_date(mtime)
        if last_modified == req.get_header('If-Modified-Since'):
            req.send_response(304)
            req.end_headers()
            return

        formatter = HtmlFormatter(style=style_cls)
        content = u'\n\n'.join([
            formatter.get_style_defs('div.code pre'),
            formatter.get_style_defs('table.code td')
        ]).encode('utf-8')

        req.send_response(200)
        req.send_header('Content-Type', 'text/css; charset=utf-8')
        req.send_header('Last-Modified', last_modified)
        req.send_header('Content-Length', len(content))
        req.write(content)

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        return [resource_filename('trac.mimeview', 'templates')]

    # Internal methods

    @lazy
    def _lexer_alias_name_map(self):
        lexer_alias_name_map = {}
        for lexer_name, aliases, _, _ in get_all_lexers():
            name = aliases[0] if aliases else lexer_name
            for alias in aliases:
                lexer_alias_name_map[alias] = name
        return lexer_alias_name_map

    @lazy
    def _lexer_options(self):
        lexer_options = {}
        for key, lexer_option_value in self.pygments_lexer_options.options():
            try:
                lexer_name_or_alias, lexer_option_name = key.split('.')
            except ValueError:
                pass
            else:
                lexer_name = self._lexer_alias_to_name(lexer_name_or_alias)
                lexer_option = {lexer_option_name: lexer_option_value}
                lexer_options.setdefault(lexer_name, {}).update(lexer_option)
        return lexer_options

    @lazy
    def _types(self):
        types = {}
        for lexer_name, aliases, _, mimetypes in get_all_lexers():
            name = aliases[0] if aliases else lexer_name
            for mimetype in mimetypes:
                types[mimetype] = (name, self.QUALITY_RATIO)

        # Pygments < 1.4 doesn't know application/javascript
        if 'application/javascript' not in types:
            js_entry = types.get('text/javascript')
            if js_entry:
                types['application/javascript'] = js_entry

        types.update(Mimeview(self.env).configured_modes_mapping('pygments'))
        return types

    def _generate(self, language, content, context=None):
        lexer_name = self._lexer_alias_to_name(language)
        lexer_options = {'stripnl': False}
        lexer_options.update(self._lexer_options.get(lexer_name, {}))
        if context:
            lexer_options.update(context.get_hint('lexer_options', {}))
        lexer = get_lexer_by_name(lexer_name, **lexer_options)
        out = io.StringIO()
        # Specify `lineseparator` to workaround exception with Pygments 2.2.0:
        # "TypeError: unicode argument expected, got 'str'" with newline input
        formatter = HtmlFormatter(nowrap=True, lineseparator=u'\n')
        formatter.format(lexer.get_tokens(content), out)
        return Markup(out.getvalue())

    def _lexer_alias_to_name(self, alias):
        return self._lexer_alias_name_map.get(alias, alias)
示例#23
0
class ProductEnvironment(Component, ComponentManager):
    """Bloodhound product-aware environment manager.

    Bloodhound encapsulates access to product resources stored inside a
    Trac environment via product environments. They are compatible lightweight
    irepresentations of top level environment. 

    Product environments contain among other things:

    * configuration key-value pairs stored in the database,
    * product-aware clones of the wiki and ticket attachments files,

    Product environments do not have:

    * product-specific templates and plugins,
    * a separate database
    * active participation in database upgrades and other setup tasks

    See https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003
    """
    class __metaclass__(ComponentMeta):
        def product_env_keymap(args, kwds, kwd_mark):
            # Remove meta-reference to self (i.e. product env class)
            args = args[1:]
            try:
                product = kwds['product']
            except KeyError:
                # Product provided as positional argument
                if isinstance(args[1], Product):
                    args = (args[0], args[1].prefix) + args[2:]
            else:
                # Product supplied as keyword argument
                if isinstance(product, Product):
                    kwds['product'] = product.prefix
            return default_keymap(args, kwds, kwd_mark)

        @lru_cache(maxsize=100, keymap=product_env_keymap)
        def __call__(self, *args, **kwargs):
            """Return an existing instance of there is a hit 
            in the global LRU cache, otherwise create a new instance.
            """
            return ComponentMeta.__call__(self, *args, **kwargs)

        del product_env_keymap

    implements(trac.env.ISystemInfoProvider, IPermissionRequestor)

    setup_participants = ExtensionPoint(trac.env.IEnvironmentSetupParticipant)
    multi_product_support_components = ExtensionPoint(
        ISupportMultiProductEnvironment)

    @property
    def product_setup_participants(self):
        return [
            component for component in self.setup_participants
            if component not in self.multi_product_support_components
        ]

    components_section = ConfigSection(
        'components', """This section is used to enable or disable components
        provided by plugins, as well as by Trac itself.

        See also: TracIni , TracPlugins
        """)

    @property
    def shared_plugins_dir():
        """Product environments may not add plugins.
        """
        return ''

    _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.""")

    @property
    def base_url(self):
        base_url = self._base_url
        if base_url == self.parent.base_url:
            return ''
        return base_url

    _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)''""")

    @property
    def project_name(self):
        """Name of the product.
        """
        return self.product.name

    @property
    def project_description(self):
        """Short description of the product.
        """
        return self.product.description

    @property
    def project_url(self):
        """URL of the main project web site, usually the website in
        which the `base_url` resides. This is used in notification
        e-mails.
        """
        # FIXME: Should products have different values i.e. config option ?
        return self.parent.project_url

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

    @property
    def project_footer(self):
        """Page footer text (right-aligned).
        """
        # FIXME: Should products have different values i.e. config option ?
        return self.parent.project_footer

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

    log_type = Option(
        'logging', 'log_type', 'inherit', """Logging facility to use.

        Should be one of (`inherit`, `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, env, product, create=False):
        """Initialize the product environment.

        :param env:     the global Trac environment
        :param product: product prefix or an instance of
                        multiproduct.model.Product
        """
        if not isinstance(env, trac.env.Environment):
            cls = self.__class__
            raise TypeError("Initializer must be called with " \
                "trac.env.Environment instance as first argument " \
                "(got %s instance instead)" %
                         (cls.__module__ + '.' + cls.__name__, ))

        ComponentManager.__init__(self)

        if isinstance(product, Product):
            if product._env is not env:
                raise ValueError("Product's environment mismatch")
        elif isinstance(product, basestring):
            products = Product.select(env, where={'prefix': product})
            if len(products) == 1:
                product = products[0]
            else:
                env.log.debug("Products for '%s' : %s", product, products)
                raise LookupError("Missing product %s" % (product, ))

        self.parent = env
        self.product = product
        self.systeminfo = []

        self.setup_config()

        # when creating product environment, invoke `IEnvironmentSetupParticipant.environment_created`
        # for all setup participants that don't support multi product environments
        if create:
            for participant in self.product_setup_participants:
                with ComponentEnvironmentContext(self, participant):
                    participant.environment_created()

    def __getitem__(self, cls):
        if issubclass(cls, trac.env.Environment):
            return self.parent
        elif cls is self.__class__:
            return self
        else:
            return ComponentManager.__getitem__(self, cls)

    def __getattr__(self, attrnm):
        """Forward attribute access request to parent environment.

        Initially this will affect the following members of
        `trac.env.Environment` class:

        system_info_providers, secure_cookies, project_admin_trac_url,
        get_system_info, get_version, get_templates_dir, get_templates_dir,
        get_log_dir, backup
        """
        try:
            if attrnm in ('parent', '_rules'):
                raise AttributeError
            return getattr(self.parent, attrnm)
        except AttributeError:
            raise AttributeError("'%s' object has no attribute '%s'" %
                                 (self.__class__.__name__, attrnm))

    def __repr__(self):
        return "<%s %s at %s>" % (self.__class__.__name__,
                                  repr(self.product.prefix), hex(id(self)))

    @lazy
    def path(self):
        """The subfolder `./products/<product prefix>` relative to the 
        top-level directory of the global environment will be the root of 
        product file system area.
        """
        folder = os.path.join(self.parent.path, 'products',
                              self.product.prefix)
        if not os.path.exists(folder):
            os.makedirs(folder)
        return folder

    # IPermissionRequestor methods
    def get_permission_actions(self):
        """Implement the product-specific `PRODUCT_ADMIN` meta permission.
        """
        actions = set()
        permsys = PermissionSystem(self)
        for requestor in permsys.requestors:
            if requestor is not self and requestor is not permsys:
                for action in requestor.get_permission_actions() or []:
                    if isinstance(action, tuple):
                        actions.add(action[0])
                    else:
                        actions.add(action)
        # PermissionSystem's method was not invoked
        actions.add('EMAIL_VIEW')
        # FIXME: should not be needed, JIC better double check
        actions.discard('TRAC_ADMIN')
        return [('PRODUCT_ADMIN', list(actions))]

    # ISystemInfoProvider methods

    # Same as parent environment's . Avoid duplicated code
    component_activated = trac.env.Environment.component_activated.im_func
    _component_name = trac.env.Environment._component_name.im_func
    _component_rules = trac.env.Environment._component_rules
    enable_component = trac.env.Environment.enable_component.im_func
    get_known_users = trac.env.Environment.get_known_users.im_func
    get_repository = trac.env.Environment.get_repository.im_func

    is_component_enabled_local = trac.env.Environment.is_component_enabled.im_func

    def is_component_enabled(self, cls):
        """Implemented to only allow activation of components already 
        activated in the global environment that are in turn 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.
        """
        if cls is self.__class__:
            # Prevent lookups in parent env ... will always fail
            return True
        # FIXME : Maybe checking for ComponentManager is too drastic
        elif issubclass(cls, ComponentManager):
            # Avoid clashes with overridden Environment's options
            return False
        elif self.parent[cls] is None:
            return False
        return self.is_component_enabled_local(cls)

    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 BloodhoundConnectionWrapper(self.parent.get_db_cnx(), self)

    @property
    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, e:
                ...
        """
        # exception types same as in global environment
        return self.parent.db_exc

    def with_transaction(self, db=None):
        """Decorator for transaction functions :deprecated:
        """
        raise NotImplementedError('Deprecated method')

    def get_read_db(self):
        """Return a database connection for read purposes :deprecated:

        See `trac.db.api.get_read_db` for detailed documentation.
        """
        return BloodhoundConnectionWrapper(self.parent.get_read_db(), self)

    @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 ProductEnvContextManager(QueryContextManager(self.parent), 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 ProductEnvContextManager(TransactionContextManager(self.parent),
                                        self)

    def shutdown(self, tid=None):
        """Close the environment."""
        RepositoryManager(self).shutdown(tid)
        # FIXME: Shared DB so IMO this should not happen ... at least not here
        #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 create(self, options=[]):
        """Placeholder for compatibility when trying to create the basic 
        directory structure of the environment, etc ...

        This method does nothing at all.
        """
        # TODO: Handle options args

    def setup_config(self):
        """Load the configuration object.
        """
        import trac.config

        parent_path = MultiProductSystem(self.parent).product_config_parent
        if parent_path and os.path.isfile(parent_path):
            parents = [trac.config.Configuration(parent_path)]
        else:
            parents = [self.parent.config]
        self.config = Configuration(self.parent, self.product.prefix, parents)
        self.setup_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
        format = self.log_format

        self.parent.log.debug("Log type '%s' for product '%s'", logtype,
                              self.product.prefix)

        # Force logger inheritance on identical configuration
        if (logtype, logfile,
                format) == (self.parent.log_type, self.parent.log_file,
                            self.parent.log_format):
            logtype = 'inherit'

        if logtype == 'inherit':
            self.log = self.parent.log
            self._log_handler = self.parent._log_handler
            self.parent.log.warning(
                "Inheriting parent logger for product '%s'",
                self.product.prefix)
        else:
            if logtype == 'file' and not os.path.isabs(logfile):
                logfile = os.path.join(self.get_log_dir(), logfile)
            logid = 'Trac.%s.%s' % \
                    (sha1(self.parent.path).hexdigest(), self.product.prefix)
            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, logid, format=format)

        from trac import core, __version__ as VERSION
        self.log.info(
            '-' * 32 + ' product %s environment startup [Trac %s] ' + '-' * 32,
            self.product.prefix,
            get_pkginfo(core).get('version', VERSION))

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        # Upgrades are handled by global environment
        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
        """
        # Upgrades handled by global environment
        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:
            urlpattern = MultiProductSystem(self.parent).product_base_url
            if not urlpattern:
                self.log.warn("product_base_url option not set in "
                              "configuration, generated links may be "
                              "incorrect")
                urlpattern = 'products/$(prefix)s'
            envname = os.path.basename(self.parent.path)
            prefix = unicode_quote(self.product.prefix, safe="")
            name = unicode_quote(self.product.name, safe="")
            url = urlpattern.replace('$(', '%(') \
                            .replace('%(envname)s', envname) \
                            .replace('%(prefix)s', prefix) \
                            .replace('%(name)s', name)
            if urlsplit(url).netloc:
                #  Absolute URLs
                _abs_href = Href(url)
            else:
                # Relative URLs
                parent_href = Href(self.parent.abs_href(),
                                   path_safe="/!~*'()%",
                                   query_safe="!~*'()%")
                _abs_href = Href(parent_href(url))
        else:
            _abs_href = Href(self.base_url)
        return _abs_href

    # Multi-product API extensions

    @classmethod
    def lookup_global_env(cls, env):
        return env.parent if isinstance(env, ProductEnvironment) else env

    @classmethod
    def lookup_env(cls, env, prefix=None, name=None):
        """Instantiate environment according to product prefix or name

        @throws LookupError if no product matches neither prefix nor name 
        """
        if isinstance(env, ProductEnvironment):
            global_env = env.parent
        else:
            global_env = env

        # FIXME: Update if multiproduct.dbcursor.GLOBAL_PRODUCT != ''
        if not prefix and not name:
            return global_env
        elif isinstance(env, ProductEnvironment) and \
                env.product.prefix == prefix:
            return env
        if prefix:
            try:
                return ProductEnvironment(global_env, prefix)
            except LookupError:
                if not name:
                    raise
        if name:
            # Lookup product by name
            products = Product.select(global_env, where={'name': name})
            if products:
                return ProductEnvironment(global_env, products[0])
            else:
                raise LookupError("Missing product '%s'" % (name, ))
        else:
            raise LookupError("Mising product '%s'" % (prefix or name, ))

    @classmethod
    def resolve_href(cls, to_env, at_env):
        """Choose absolute or relative href when generating links to 
        a product (or global) environment.

        @param at_env:        href expansion is taking place in the 
                              scope of this environment 
        @param to_env:        generated URLs point to resources in
                              this environment
        """
        at_href = at_env.abs_href()
        target_href = to_env.abs_href()
        if urlsplit(at_href)[1] == urlsplit(target_href)[1]:
            return to_env.href
        else:
            return to_env.abs_href
示例#24
0
class InterTracDispatcher(Component):
    """InterTrac dispatcher."""

    implements(IRequestHandler, IWikiMacroProvider)

    is_valid_default_handler = False

    intertrac_section = ConfigSection(
        'intertrac',
        """This section configures InterTrac prefixes. Option names in
        this section that contain a `.` are of the format
        `<name>.<attribute>`. Option names that don't contain a `.` define
        an alias.

        The `.url` attribute is mandatory and is used for locating the
        other Trac. This can be a relative path when the other Trac
        environment is located on the same server.

        The `.title` attribute is used for generating a tooltip when the
        cursor is hovered over an InterTrac link.

        Example configuration:
        {{{#!ini
        [intertrac]
        # -- Example of setting up an alias:
        t = trac

        # -- Link to an external Trac:
        genshi.title = Edgewall's Trac for Genshi
        genshi.url = http://genshi.edgewall.org
        }}}
        """)

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'^/intertrac/(.*)', req.path_info)
        if match:
            if match.group(1):
                req.args['link'] = match.group(1)
            return True

    def process_request(self, req):
        link = req.args.get('link', '')
        parts = link.split(':', 1)
        if len(parts) > 1:
            resolver, target = parts
            if target[:1] + target[-1:] not in ('""', "''"):
                link = '%s:"%s"' % (resolver, target)
        from trac.web.chrome import web_context
        link_frag = extract_link(self.env, web_context(req), link)
        if isinstance(link_frag, (Element, Fragment)):
            elt = find_element(link_frag, 'href')
            if elt is None:
                raise TracError(
                    _(
                        "Can't view %(link)s. Resource doesn't exist or "
                        "you don't have the required permission.",
                        link=link))
            href = elt.attrib.get('href')
        else:
            href = req.href(link.rstrip(':'))
        req.redirect(href)

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'InterTrac'

    def get_macro_description(self, name):
        return 'messages', N_("Provide a list of known InterTrac prefixes.")

    def expand_macro(self, formatter, name, content):
        intertracs = {}
        for key, value in self.intertrac_section.options():
            idx = key.rfind('.')
            if idx > 0:  # 0 itself doesn't help much: .xxx = ...
                prefix, attribute = key[:idx], key[idx + 1:]
                intertrac = intertracs.setdefault(prefix, {})
                intertrac[attribute] = value
            else:
                intertracs[key] = value  # alias
        if 'trac' not in intertracs:
            intertracs['trac'] = {
                'title': _('The Trac Project'),
                'url': 'http://trac.edgewall.org'
            }

        def generate_prefix(prefix):
            intertrac = intertracs[prefix]
            if isinstance(intertrac, basestring):
                yield tag.tr(
                    tag.td(tag.strong(prefix)),
                    tag.td(
                        tag_("Alias for %(name)s",
                             name=tag.strong(intertrac))))
            else:
                url = intertrac.get('url', '')
                if url:
                    title = intertrac.get('title', url)
                    yield tag.tr(
                        tag.td(
                            tag.a(tag.strong(prefix), href=url + '/timeline')),
                        tag.td(tag.a(title, href=url)))

        return tag.table(class_="wiki intertrac")(
            tag.tr(tag.th(tag.em(_("Prefix"))),
                   tag.th(tag.em(_("Trac Site")))),
            [generate_prefix(p) for p in sorted(intertracs.keys())])
示例#25
0
class TicketSystem(Component):
    implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager,
               ITicketManipulator)

    change_listeners = ExtensionPoint(ITicketChangeListener)
    milestone_change_listeners = ExtensionPoint(IMilestoneChangeListener)

    realm = 'ticket'

    ticket_custom_section = ConfigSection(
        'ticket-custom',
        """In this section, you can define additional fields for tickets. See
        TracTicketsCustomFields for more details.""")

    action_controllers = OrderedExtensionsOption(
        'ticket',
        'workflow',
        ITicketActionController,
        default='ConfigurableTicketWorkflow',
        include_missing=False,
        doc="""Ordered list of workflow controllers to use for ticket actions.
            """)

    restrict_owner = BoolOption(
        'ticket', 'restrict_owner', 'false',
        """Make the owner field of tickets use a drop-down menu.
        Be sure to understand the performance implications before activating
        this option. See
        [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List].

        Please note that e-mail addresses are '''not''' obfuscated in the
        resulting drop-down menu, so this option should not be used if
        e-mail addresses must remain protected.
        """)

    default_version = Option('ticket', 'default_version', '',
                             """Default version for newly created tickets.""")

    default_type = Option('ticket', 'default_type', 'defect',
                          """Default type for newly created tickets.""")

    default_priority = Option(
        'ticket', 'default_priority', 'major',
        """Default priority for newly created tickets.""")

    default_milestone = Option(
        'ticket', 'default_milestone', '',
        """Default milestone for newly created tickets.""")

    default_component = Option(
        'ticket', 'default_component', '',
        """Default component for newly created tickets.""")

    default_severity = Option(
        'ticket', 'default_severity', '',
        """Default severity for newly created tickets.""")

    default_summary = Option(
        'ticket', 'default_summary', '',
        """Default summary (title) for newly created tickets.""")

    default_description = Option(
        'ticket', 'default_description', '',
        """Default description for newly created tickets.""")

    default_keywords = Option(
        'ticket', 'default_keywords', '',
        """Default keywords for newly created tickets.""")

    default_owner = Option(
        'ticket', 'default_owner', '< default >',
        """Default owner for newly created tickets. The component owner
        is used when set to the value `< default >`.
        """)

    default_cc = Option('ticket', 'default_cc', '',
                        """Default cc: list for newly created tickets.""")

    default_resolution = Option(
        'ticket', 'default_resolution', 'fixed',
        """Default resolution for resolving (closing) tickets.""")

    allowed_empty_fields = ListOption(
        'ticket',
        'allowed_empty_fields',
        'milestone, version',
        doc="""Comma-separated list of `select` fields that can have
        an empty value. (//since 1.1.2//)""")

    max_comment_size = IntOption(
        'ticket', 'max_comment_size', 262144,
        """Maximum allowed comment size in characters.""")

    max_description_size = IntOption(
        'ticket', 'max_description_size', 262144,
        """Maximum allowed description size in characters.""")

    max_summary_size = IntOption(
        'ticket', 'max_summary_size', 262144,
        """Maximum allowed summary size in characters. (//since 1.0.2//)""")

    def __init__(self):
        self.log.debug('action controllers for ticket workflow: %r',
                       [c.__class__.__name__ for c in self.action_controllers])

    # Public API

    def get_available_actions(self, req, ticket):
        """Returns a sorted list of available actions"""
        # The list should not have duplicates.
        actions = {}
        for controller in self.action_controllers:
            weighted_actions = controller.get_ticket_actions(req, ticket) or []
            for weight, action in weighted_actions:
                if action in actions:
                    actions[action] = max(actions[action], weight)
                else:
                    actions[action] = weight
        all_weighted_actions = [(weight, action)
                                for action, weight in actions.items()]
        return [x[1] for x in sorted(all_weighted_actions, reverse=True)]

    def get_all_status(self):
        """Returns a sorted list of all the states all of the action
        controllers know about."""
        valid_states = set()
        for controller in self.action_controllers:
            valid_states.update(controller.get_all_status() or [])
        return sorted(valid_states)

    def get_ticket_field_labels(self):
        """Produce a (name,label) mapping from `get_ticket_fields`."""
        labels = {f['name']: f['label'] for f in self.get_ticket_fields()}
        labels['attachment'] = _("Attachment")
        return labels

    def get_ticket_fields(self):
        """Returns list of fields available for tickets.

        Each field is a dict with at least the 'name', 'label' (localized)
        and 'type' keys.
        It may in addition contain the 'custom' key, the 'optional' and the
        'options' keys. When present 'custom' and 'optional' are always `True`.
        """
        fields = copy.deepcopy(self.fields)
        label = 'label'  # workaround gettext extraction bug
        for f in fields:
            f[label] = gettext(f[label])
        return fields

    def reset_ticket_fields(self):
        """Invalidate ticket field cache."""
        del self.fields

    @cached
    def fields(self):
        """Return the list of fields available for tickets."""
        from trac.ticket import model

        fields = TicketFieldList()

        # Basic text fields
        fields.append({
            'name': 'summary',
            'type': 'text',
            'label': N_('Summary')
        })
        fields.append({
            'name': 'reporter',
            'type': 'text',
            'label': N_('Reporter')
        })

        # Owner field, by default text but can be changed dynamically
        # into a drop-down depending on configuration (restrict_owner=true)
        fields.append({'name': 'owner', 'type': 'text', 'label': N_('Owner')})

        # Description
        fields.append({
            'name': 'description',
            'type': 'textarea',
            'format': 'wiki',
            'label': N_('Description')
        })

        # Default select and radio fields
        selects = [('type', N_('Type'), model.Type),
                   ('status', N_('Status'), model.Status),
                   ('priority', N_('Priority'), model.Priority),
                   ('milestone', N_('Milestone'), model.Milestone),
                   ('component', N_('Component'), model.Component),
                   ('version', N_('Version'), model.Version),
                   ('severity', N_('Severity'), model.Severity),
                   ('resolution', N_('Resolution'), model.Resolution)]
        for name, label, cls in selects:
            options = [val.name for val in cls.select(self.env)]
            if not options:
                # Fields without possible values are treated as if they didn't
                # exist
                continue
            field = {
                'name': name,
                'type': 'select',
                'label': label,
                'value': getattr(self, 'default_' + name, ''),
                'options': options
            }
            if name in ('status', 'resolution'):
                field['type'] = 'radio'
                field['optional'] = True
            elif name in self.allowed_empty_fields:
                field['optional'] = True
            fields.append(field)

        # Advanced text fields
        fields.append({
            'name': 'keywords',
            'type': 'text',
            'format': 'list',
            'label': N_('Keywords')
        })
        fields.append({
            'name': 'cc',
            'type': 'text',
            'format': 'list',
            'label': N_('Cc')
        })

        # Date/time fields
        fields.append({
            'name': 'time',
            'type': 'time',
            'format': 'relative',
            'label': N_('Created')
        })
        fields.append({
            'name': 'changetime',
            'type': 'time',
            'format': 'relative',
            'label': N_('Modified')
        })

        for field in self.custom_fields:
            if field['name'] in [f['name'] for f in fields]:
                self.log.warning('Duplicate field name "%s" (ignoring)',
                                 field['name'])
                continue
            fields.append(field)

        return fields

    reserved_field_names = [
        'report', 'order', 'desc', 'group', 'groupdesc', 'col', 'row',
        'format', 'max', 'page', 'verbose', 'comment', 'or', 'id', 'time',
        'changetime', 'owner', 'reporter', 'cc', 'summary', 'description',
        'keywords'
    ]

    def get_custom_fields(self):
        return copy.deepcopy(self.custom_fields)

    @cached
    def custom_fields(self):
        """Return the list of custom ticket fields available for tickets."""
        fields = TicketFieldList()
        config = self.ticket_custom_section
        for name in [
                option for option, value in config.options()
                if '.' not in option
        ]:
            field = {
                'name':
                name,
                'custom':
                True,
                'type':
                config.get(name),
                'order':
                config.getint(name + '.order', 0),
                'label':
                config.get(name + '.label')
                or name.replace("_", " ").strip().capitalize(),
                'value':
                config.get(name + '.value', '')
            }
            if field['type'] == 'select' or field['type'] == 'radio':
                field['options'] = config.getlist(name + '.options', sep='|')
                if '' in field['options'] or \
                        field['name'] in self.allowed_empty_fields:
                    field['optional'] = True
                    if '' in field['options']:
                        field['options'].remove('')
            elif field['type'] == 'checkbox':
                field['value'] = '1' if as_bool(field['value']) else '0'
            elif field['type'] == 'text':
                field['format'] = config.get(name + '.format', 'plain')
                field['max_size'] = config.getint(name + '.max_size', 0)
            elif field['type'] == 'textarea':
                field['format'] = config.get(name + '.format', 'plain')
                field['max_size'] = config.getint(name + '.max_size', 0)
                field['height'] = config.getint(name + '.rows')
            elif field['type'] == 'time':
                field['format'] = config.get(name + '.format', 'datetime')

            if field['name'] in self.reserved_field_names:
                self.log.warning(
                    'Field name "%s" is a reserved name '
                    '(ignoring)', field['name'])
                continue
            if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']):
                self.log.warning(
                    'Invalid name for custom field: "%s" '
                    '(ignoring)', field['name'])
                continue

            fields.append(field)

        fields.sort(key=lambda f: (f['order'], f['name']))
        return fields

    def get_field_synonyms(self):
        """Return a mapping from field name synonyms to field names.
        The synonyms are supposed to be more intuitive for custom queries."""
        # i18n TODO - translated keys
        return {'created': 'time', 'modified': 'changetime'}

    def eventually_restrict_owner(self, field, ticket=None):
        """Restrict given owner field to be a list of users having
        the TICKET_MODIFY permission (for the given ticket)
        """
        if self.restrict_owner:
            field['type'] = 'select'
            field['options'] = self.get_allowed_owners(ticket)
            field['optional'] = True

    def get_allowed_owners(self, ticket=None):
        """Returns a list of permitted ticket owners (those possessing the
        TICKET_MODIFY permission). Returns `None` if the option `[ticket]`
        `restrict_owner` is `False`.

        If `ticket` is not `None`, fine-grained permission checks are used
        to determine the allowed owners for the specified resource.

        :since: 1.0.3
        """
        if self.restrict_owner:
            allowed_owners = []
            for user in PermissionSystem(self.env) \
                        .get_users_with_permission('TICKET_MODIFY'):
                if not ticket or \
                        'TICKET_MODIFY' in PermissionCache(self.env, user,
                                                           ticket.resource):
                    allowed_owners.append(user)
            allowed_owners.sort()
            return allowed_owners

    # ITicketManipulator methods

    def prepare_ticket(self, req, ticket, fields, actions):
        pass

    def validate_ticket(self, req, ticket):
        # Validate select fields for known values.
        for field in ticket.fields:
            if 'options' not in field:
                continue
            name = field['name']
            if name == 'status':
                continue
            if name in ticket and name in ticket._old:
                value = ticket[name]
                if value:
                    if value not in field['options']:
                        yield name, _('"%(value)s" is not a valid value',
                                      value=value)
                elif not field.get('optional', False):
                    yield name, _("field cannot be empty")

        # Validate description length.
        if len(ticket['description'] or '') > self.max_description_size:
            yield 'description', _(
                "Must be less than or equal to %(num)s "
                "characters",
                num=self.max_description_size)

        # Validate summary length.
        if not ticket['summary']:
            yield 'summary', _("Tickets must contain a summary.")
        elif len(ticket['summary'] or '') > self.max_summary_size:
            yield 'summary', _(
                "Must be less than or equal to %(num)s "
                "characters",
                num=self.max_summary_size)

        # Validate custom field length.
        for field in ticket.custom_fields:
            field_attrs = ticket.fields.by_name(field)
            max_size = field_attrs.get('max_size', 0)
            if 0 < max_size < len(ticket[field] or ''):
                label = field_attrs.get('label')
                yield label or field, _(
                    "Must be less than or equal to "
                    "%(num)s characters",
                    num=max_size)

        # Validate time field content.
        for field in ticket.time_fields:
            value = ticket[field]
            if field in ticket.custom_fields and \
                    field in ticket._old and \
                    not isinstance(value, datetime):
                field_attrs = ticket.fields.by_name(field)
                format = field_attrs.get('format')
                try:
                    ticket[field] = user_time(req, parse_date, value,
                                              hint=format) \
                                    if value else None
                except TracError as e:
                    # Degrade TracError to warning.
                    ticket[field] = value
                    label = field_attrs.get('label')
                    yield label or field, to_unicode(e)

    def validate_comment(self, req, comment):
        # Validate comment length
        if len(comment or '') > self.max_comment_size:
            yield _("Must be less than or equal to %(num)s characters",
                    num=self.max_comment_size)

    # IPermissionRequestor methods

    def get_permission_actions(self):
        return [
            'TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP', 'TICKET_VIEW',
            'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION', 'TICKET_EDIT_COMMENT',
            ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']),
            ('TICKET_ADMIN', [
                'TICKET_CREATE', 'TICKET_MODIFY', 'TICKET_VIEW',
                'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION',
                'TICKET_EDIT_COMMENT'
            ])
        ]

    # IWikiSyntaxProvider methods

    def get_link_resolvers(self):
        return [('bug', self._format_link), ('issue', self._format_link),
                ('ticket', self._format_link),
                ('comment', self._format_comment_link)]

    def get_wiki_syntax(self):
        yield (
            # matches #... but not &#... (HTML entity)
            r"!?(?<!&)#"
            # optional intertrac shorthand #T... + digits
            r"(?P<it_ticket>%s)%s" %
            (WikiParser.INTERTRAC_SCHEME, Ranges.RE_STR),
            lambda x, y, z: self._format_link(x, 'ticket', y[1:], y, z))

    def _format_link(self, formatter, ns, target, label, fullmatch=None):
        intertrac = formatter.shorthand_intertrac_helper(
            ns, target, label, fullmatch)
        if intertrac:
            return intertrac
        try:
            link, params, fragment = formatter.split_link(target)
            r = Ranges(link)
            if len(r) == 1:
                num = r.a
                ticket = formatter.resource(self.realm, num)
                from trac.ticket.model import Ticket
                if Ticket.id_is_valid(num) and \
                        'TICKET_VIEW' in formatter.perm(ticket):
                    # TODO: attempt to retrieve ticket view directly,
                    #       something like: t = Ticket.view(num)
                    for type, summary, status, resolution in \
                            self.env.db_query("""
                            SELECT type, summary, status, resolution
                            FROM ticket WHERE id=%s
                            """, (str(num),)):
                        description = self.format_summary(
                            summary, status, resolution, type)
                        title = '#%s: %s' % (num, description)
                        href = formatter.href.ticket(num) + params + fragment
                        return tag.a(label,
                                     title=title,
                                     href=href,
                                     class_='%s ticket' % status)
            else:
                ranges = str(r)
                if params:
                    params = '&' + params[1:]
                label_wrap = label.replace(',', u',\u200b')
                ranges_wrap = ranges.replace(',', u', ')
                return tag.a(label_wrap,
                             title=_("Tickets %(ranges)s", ranges=ranges_wrap),
                             href=formatter.href.query(id=ranges) + params)
        except ValueError:
            pass
        return tag.a(label, class_='missing ticket')

    def _format_comment_link(self, formatter, ns, target, label):
        resource = None
        if ':' in target:
            elts = target.split(':')
            if len(elts) == 3:
                cnum, realm, id = elts
                if cnum != 'description' and cnum and not cnum[0].isdigit():
                    realm, id, cnum = elts  # support old comment: style
                id = as_int(id, None)
                if realm in ('bug', 'issue'):
                    realm = 'ticket'
                resource = formatter.resource(realm, id)
        else:
            resource = formatter.resource
            cnum = target

        if resource and resource.id and resource.realm == self.realm and \
                cnum and (cnum.isdigit() or cnum == 'description'):
            href = title = class_ = None
            if self.resource_exists(resource):
                from trac.ticket.model import Ticket
                ticket = Ticket(self.env, resource.id)
                if cnum != 'description' and not ticket.get_change(cnum):
                    title = _("ticket comment does not exist")
                    class_ = 'missing ticket'
                elif 'TICKET_VIEW' in formatter.perm(resource):
                    href = formatter.href.ticket(resource.id) + \
                           "#comment:%s" % cnum
                    if resource.id != formatter.resource.id:
                        summary = self.format_summary(ticket['summary'],
                                                      ticket['status'],
                                                      ticket['resolution'],
                                                      ticket['type'])
                        if cnum == 'description':
                            title = _("Description for #%(id)s: %(summary)s",
                                      id=resource.id,
                                      summary=summary)
                        else:
                            title = _(
                                "Comment %(cnum)s for #%(id)s: "
                                "%(summary)s",
                                cnum=cnum,
                                id=resource.id,
                                summary=summary)
                        class_ = ticket['status'] + ' ticket'
                    else:
                        title = _("Description") if cnum == 'description' \
                                                 else _("Comment %(cnum)s",
                                                        cnum=cnum)
                        class_ = 'ticket'
                else:
                    title = _("no permission to view ticket")
                    class_ = 'forbidden ticket'
            else:
                title = _("ticket does not exist")
                class_ = 'missing ticket'
            return tag.a(label, class_=class_, href=href, title=title)
        return label

    # IResourceManager methods

    def get_resource_realms(self):
        yield self.realm

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        if format == 'compact':
            return '#%s' % resource.id
        elif format == 'summary':
            from trac.ticket.model import Ticket
            ticket = Ticket(self.env, resource.id)
            args = [
                ticket[f] for f in ('summary', 'status', 'resolution', 'type')
            ]
            return self.format_summary(*args)
        return _("Ticket #%(shortname)s", shortname=resource.id)

    def format_summary(self, summary, status=None, resolution=None, type=None):
        summary = shorten_line(summary)
        if type:
            summary = type + ': ' + summary
        if status:
            if status == 'closed' and resolution:
                status += ': ' + resolution
            return "%s (%s)" % (summary, status)
        else:
            return summary

    def resource_exists(self, resource):
        """
        >>> from trac.test import EnvironmentStub
        >>> from trac.resource import Resource, resource_exists
        >>> env = EnvironmentStub()

        >>> resource_exists(env, Resource('ticket', 123456))
        False

        >>> from trac.ticket.model import Ticket
        >>> t = Ticket(env)
        >>> int(t.insert())
        1
        >>> resource_exists(env, t.resource)
        True
        """
        try:
            id_ = int(resource.id)
        except (TypeError, ValueError):
            return False
        if self.env.db_query("SELECT id FROM ticket WHERE id=%s", (id_, )):
            if resource.version is None:
                return True
            revcount = self.env.db_query(
                """
                SELECT count(DISTINCT time) FROM ticket_change WHERE ticket=%s
                """, (id_, ))
            return revcount[0][0] >= resource.version
        else:
            return False
示例#26
0
class DefaultTicketGroupStatsProvider(Component):
    """Configurable ticket group statistics provider.

    Example configuration (which is also the default):
    {{{
    [milestone-groups]

    # Definition of a 'closed' group:
    
    closed = closed

    # The definition consists in a comma-separated list of accepted status.
    # Also, '*' means any status and could be used to associate all remaining
    # states to one catch-all group.

    # Qualifiers for the above group (the group must have been defined first):
    
    closed.order = 0                     # sequence number in the progress bar
    closed.query_args = group=resolution # optional extra param for the query
    closed.overall_completion = true     # count for overall completion

    # Definition of an 'active' group:

    active = *                           # one catch-all group is allowed
    active.order = 1
    active.css_class = open              # CSS class for this interval
    active.label = in progress           # Displayed name for the group,
                                         #  needed for non-ascii group names

    # The CSS class can be one of: new (yellow), open (no color) or
    # closed (green). New styles can easily be added using the following
    # selector:  `table.progress td.<class>`
    }}}
    """

    implements(ITicketGroupStatsProvider)

    milestone_groups_section = ConfigSection(
        'milestone-groups',
        """As the workflow for tickets is now configurable, there can be many
        ticket states, and simply displaying closed tickets vs. all the others
        is maybe not appropriate in all cases. This section enables one to
        easily create ''groups'' of states that will be shown in different
        colors in the milestone progress bar.
        
        Example configuration (the default only has closed and active):
        {{{
        closed = closed
        # sequence number in the progress bar
        closed.order = 0
        # optional extra param for the query (two additional columns: created and modified and sort on created)
        closed.query_args = group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime
        # indicates groups that count for overall completion percentage
        closed.overall_completion = true
        
        new = new
        new.order = 1
        new.css_class = new
        new.label = new
        
        # one catch-all group is allowed
        active = *
        active.order = 2
        # CSS class for this interval
        active.css_class = open
        # Displayed label for this group
        active.label = in progress
        }}}
        
        The definition consists in a comma-separated list of accepted status.
        Also, '*' means any status and could be used to associate all remaining
        states to one catch-all group.
        
        The CSS class can be one of: new (yellow), open (no color) or closed
        (green). New styles can easily be added using the following selector:
        `table.progress td.<class>`
        
        (''since 0.11'')""")

    default_milestone_groups = [{
        'name': 'closed',
        'status': 'closed',
        'query_args': 'group=resolution',
        'overall_completion': 'true'
    }, {
        'name': 'active',
        'status': '*',
        'css_class': 'open'
    }]

    def _get_ticket_groups(self):
        """Returns a list of dict describing the ticket groups
        in the expected order of appearance in the milestone progress bars.
        """
        if 'milestone-groups' in self.config:
            groups = {}
            order = 0
            for groupname, value in self.milestone_groups_section.options():
                qualifier = 'status'
                if '.' in groupname:
                    groupname, qualifier = groupname.split('.', 1)
                group = groups.setdefault(groupname, {
                    'name': groupname,
                    'order': order
                })
                group[qualifier] = value
                order = max(order, int(group['order'])) + 1
            return [
                group for group in sorted(groups.values(),
                                          key=lambda g: int(g['order']))
            ]
        else:
            return self.default_milestone_groups

    def get_ticket_group_stats(self, ticket_ids):
        total_cnt = len(ticket_ids)
        all_statuses = set(TicketSystem(self.env).get_all_status())
        status_cnt = {}
        for s in all_statuses:
            status_cnt[s] = 0
        if total_cnt:
            for status, count in self.env.db_query("""
                    SELECT status, count(status) FROM ticket
                    WHERE id IN (%s) GROUP BY status
                    """ % ",".join(str(x) for x in sorted(ticket_ids))):
                status_cnt[status] = count

        stat = TicketGroupStats(_('ticket status'), _('tickets'))
        remaining_statuses = set(all_statuses)
        groups = self._get_ticket_groups()
        catch_all_group = None
        # we need to go through the groups twice, so that the catch up group
        # doesn't need to be the last one in the sequence
        for group in groups:
            status_str = group['status'].strip()
            if status_str == '*':
                if catch_all_group:
                    raise TracError(
                        _(
                            "'%(group1)s' and '%(group2)s' milestone groups "
                            "both are declared to be \"catch-all\" groups. "
                            "Please check your configuration.",
                            group1=group['name'],
                            group2=catch_all_group['name']))
                catch_all_group = group
            else:
                group_statuses = set([s.strip()
                                      for s in status_str.split(',')]) \
                                      & all_statuses
                if group_statuses - remaining_statuses:
                    raise TracError(
                        _(
                            "'%(groupname)s' milestone group reused status "
                            "'%(status)s' already taken by other groups. "
                            "Please check your configuration.",
                            groupname=group['name'],
                            status=', '.join(group_statuses -
                                             remaining_statuses)))
                else:
                    remaining_statuses -= group_statuses
                group['statuses'] = group_statuses
        if catch_all_group:
            catch_all_group['statuses'] = remaining_statuses
        for group in groups:
            group_cnt = 0
            query_args = {}
            for s, cnt in status_cnt.iteritems():
                if s in group['statuses']:
                    group_cnt += cnt
                    query_args.setdefault('status', []).append(s)
            for arg in [
                    kv for kv in group.get('query_args', '').split(',')
                    if '=' in kv
            ]:
                k, v = [a.strip() for a in arg.split('=', 1)]
                query_args.setdefault(k, []).append(v)
            stat.add_interval(group.get('label', group['name']),
                              group_cnt, query_args,
                              group.get('css_class', group['name']),
                              as_bool(group.get('overall_completion')))
        stat.refresh_calcs()
        return stat
示例#27
0
class RepositoryManager(Component):
    """Version control system manager."""

    implements(IRequestFilter, IResourceManager, IRepositoryProvider)

    connectors = ExtensionPoint(IRepositoryConnector)
    providers = ExtensionPoint(IRepositoryProvider)
    change_listeners = ExtensionPoint(IRepositoryChangeListener)

    repositories_section = ConfigSection(
        'repositories',
        """One of the alternatives for registering new repositories is to
        populate the `[repositories]` section of the `trac.ini`.

        This is especially suited for setting up convenience aliases,
        short-lived repositories, or during the initial phases of an
        installation.

        See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details
        about the format adopted for this section and the rest of that page for
        the other alternatives.
        
        (''since 0.12'')""")

    repository_type = Option(
        'trac', 'repository_type', 'svn',
        """Default repository connector type. (''since 0.10'')
        
        This is also used as the default repository type for repositories
        defined in [[TracIni#repositories-section repositories]] or using the
        "Repositories" admin panel. (''since 0.12'')
        """)

    repository_dir = Option(
        'trac', 'repository_dir', '',
        """Path to the default repository. This can also be a relative path
        (''since 0.11'').
        
        This option is deprecated, and repositories should be defined in the
        [TracIni#repositories-section repositories] section, or using the
        "Repositories" admin panel. (''since 0.12'')""")

    repository_sync_per_request = ListOption(
        'trac',
        'repository_sync_per_request',
        '(default)',
        doc="""List of repositories that should be synchronized on every page
        request.
        
        Leave this option empty if you have set up post-commit hooks calling
        `trac-admin $ENV changeset added` on all your repositories
        (recommended). Otherwise, set it to a comma-separated list of
        repository names. Note that this will negatively affect performance,
        and will prevent changeset listeners from receiving events from the
        repositories specified here. The default is to synchronize the default
        repository, for backward compatibility. (''since 0.12'')""")

    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()
        self._connectors = None
        self._all_repositories = None

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        from trac.web.chrome import Chrome, add_warning
        if handler is not Chrome(self.env):
            for reponame in self.repository_sync_per_request:
                start = time.time()
                if is_default(reponame):
                    reponame = ''
                try:
                    repo = self.get_repository(reponame)
                    if repo:
                        repo.sync()
                    else:
                        self.log.warning(
                            "Unable to find repository '%s' for "
                            "synchronization", reponame or '(default)')
                        continue
                except TracError, e:
                    add_warning(
                        req,
                        _(
                            "Can't synchronize with repository \"%(name)s\" "
                            "(%(error)s). Look in the Trac log for more "
                            "information.",
                            name=reponame or '(default)',
                            error=to_unicode(e.message)))
                self.log.info("Synchronized '%s' repository in %0.2f seconds",
                              reponame or '(default)',
                              time.time() - start)
        return handler
示例#28
0
class IniEditorBasicSecurityManager(Component):
    """ Reads the option restrictions from the `trac.ini`. They're read from the
    section `[ini-editor-restrictions]`. Each option is defined as
    `<section-name>|<option-name>` and the value is either 'hidden' (option can
    neither be seen nor changed), 'readonly' (option can be seen but not changed),
    or 'modifiable' (option can be seen and changed). Section-wide access can be
    specified by `<section-name>|*`. The default value for options not specified
    can be set by `default-access` in `[ini-editor-restrictions]`. Setting it to
    `modifiable` results in specifying a "black-list", setting it to one of the
    other two values resuls in specifying a "white-list".
    """

    implements(IOptionSecurityManager)

    DEFAULT_RESTRICTIONS = {
        'ini-editor': {
            '*': ACCESS_READONLY,
            'password-options': ACCESS_MODIFIABLE
        },
        'ini-editor-restrictions': {
            '*': ACCESS_READONLY
        },
        'trac': {
            'database': ACCESS_HIDDEN  # <- may contain the database password
        }
    }

    choices = [ACCESS_READONLY, ACCESS_HIDDEN, ACCESS_MODIFIABLE]
    default_access = ChoiceOption(
        'ini-editor-restrictions',
        'default-access',
        choices,
        doc="""Defines the default access level for options that don't have an
        explicit access level defined. Defaults to readonly.""",
        doc_domain="iniadminpanel")

    ini_section = ConfigSection(
        'ini-editor-restrictions',
        """This section is used to store restriction configurations used by
        TracIniAdminPanel plugin security manager. An example file can be found at
        http://trac-hacks.org/browser/traciniadminpanelplugin/0.12/safe-restrictions.ini""",
        doc_domain='iniadminpanel')

    def __init__(self):
        restrictions = self.config.options('ini-editor-restrictions')

        self.restrictions = copy.deepcopy(self.DEFAULT_RESTRICTIONS)

        for restriction_on, level in restrictions:
            if restriction_on == 'default-access':
                continue

            # NOTE: A dot seems to be a valid character in a option name (see
            #  [ticket] -> commit_ticket_update_commands.close). A colon (':') is
            #  considered an assignment operator. So we use the pipe ('|') char in
            #  the hopes that it won't be used anywhere else. But to be on the safe
            #  side we allow it in option names.
            parts = restriction_on.split('|', 2)
            if len(parts) < 2:
                self.log.warning('Invalid restriction name: ' + restriction_on)
                continue  # no pipes in this name; so this is no valid restriction name.
                # Note that the name may contain more than one pipe if the
                # option name contains pipe chars.

            if level != ACCESS_HIDDEN and level != ACCESS_READONLY and level != ACCESS_MODIFIABLE:
                self.log.warning('Invalid restriction level for ' +
                                 restriction_on + ': ' + level)
                continue

            if parts[0] not in self.restrictions:
                self.restrictions[parts[0]] = {parts[1]: level}
            else:
                self.restrictions[parts[0]][parts[1]] = level

    def get_option_access(self, section_name, option_name):
        """Returns the access status for this option.

        Must return one of ACCESS_HIDDEN (option can neither be seen nor
        changed), ACCESS_READONLY (option can be seen but not changed), or
        ACCESS_MODIFIABLE (option can be seen and changed).
        """
        section_restrictions = self.restrictions.get(section_name.lower(),
                                                     None)
        if section_restrictions is None:
            return self.default_access

        # Return access level with fallbacks
        return section_restrictions.get(
            option_name, section_restrictions.get('*', self.default_access))

    def is_value_valid(self, section_name, option_name, option_value):
        """Checks whether the specified value is valid for the specified option.
           This can be used for example to restrict system paths to a certain
           parent directory. Will also be used against default values, if they're
           to be used.

           Returns `True` (valid) or `False` (invalid) as first return value and
           a (possibly empty) string as second return value containing the reason
           why this value is invalid.

           Note that this method is only called if the user has actually write
           permissions to this option.

           Note also that this method is only called for changed values. So if the
           `trac.ini` already contains invalid values, then they won't be checked.
        """
        return True, None
示例#29
0
    def expand_macro(self, formatter, name, content):
        from trac.config import ConfigSection, Option

        args, kw = parse_args(content)
        filters = {}
        for name, index in (('section', 0), ('option', 1)):
            pattern = kw.get(name, '').strip()
            if pattern:
                filters[name] = fnmatch.translate(pattern)
                continue
            prefix = args[index].strip() if index < len(args) else ''
            if prefix:
                filters[name] = re.escape(prefix)
        has_option_filter = 'option' in filters
        for name in ('section', 'option'):
            filters[name] = re.compile(filters[name], re.IGNORECASE).match \
                            if name in filters \
                            else lambda v: True
        section_filter = filters['section']
        option_filter = filters['option']

        section_registry = ConfigSection.get_registry(self.compmgr)
        option_registry = Option.get_registry(self.compmgr)
        options = {}
        for (section, key), option in option_registry.iteritems():
            if section_filter(section) and option_filter(key):
                options.setdefault(section, {})[key] = option
        if not has_option_filter:
            for section in section_registry:
                if section_filter(section):
                    options.setdefault(section, {})
        for section in options:
            options[section] = sorted(options[section].itervalues(),
                                      key=lambda option: option.name)
        sections = [(section, section_registry[section].doc
                              if section in section_registry else '')
                    for section in sorted(options)]

        def default_cell(option):
            default = option.default
            if default is not None and default != '':
                return tag.td(tag.code(option.dumps(default)),
                              class_='default')
            else:
                return tag.td(_("(no default)"), class_='nodefault')

        def options_table(section, options):
            if options:
                return tag.table(class_='wiki')(
                    tag.tbody(
                        tag.tr(
                            tag.td(tag.a(tag.code(option.name),
                                         class_='tracini-option',
                                         href='#%s-%s-option' %
                                              (section, option.name))),
                            tag.td(format_to_html(self.env, formatter.context,
                                                  option.doc)),
                            default_cell(option),
                            id='%s-%s-option' % (section, option.name),
                            class_='odd' if idx % 2 else 'even')
                     for idx, option in enumerate(options)))

        return tag.div(class_='tracini')(
            (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
             format_to_html(self.env, formatter.context, section_doc),
             options_table(section, options.get(section)))
            for section, section_doc in sections)
示例#30
0
文件: env.py 项目: t2y/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
        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.log = None
        self.config = None
        # System info should be provided through ISystemInfoProvider rather
        # than appending to systeminfo, which may be a private in a future
        # release.
        self.systeminfo = []

        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

    def get_configinfo(self):
        """Returns a list of dictionaries containing the `name` and `options`
        of each configuration section. The value of `options` is a list of
        dictionaries containing the `name`, `value` and `modified` state of
        each configuration option. The `modified` value is True if the value
        differs from its default.

        :since: version 1.1.2
        """
        defaults = self.config.defaults(self.compmgr)
        sections = []
        for section in self.config.sections(self.compmgr):
            options = []
            default_options = defaults.get(section, {})
            for name, value in self.config.options(section, self.compmgr):
                default = default_options.get(name) or ''
                options.append({
                    'name': name, 'value': value,
                    'modified': unicode(value) != unicode(default)
                })
            options.sort(key=lambda o: o['name'])
            sections.append({'name': section, 'options': options})
        sections.sort(key=lambda s: s['name'])
        return sections

    # 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__
        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():
            if name.endswith('.*'):
                name = name[:-2]
            _rules[name.lower()] = value.lower() in ('enabled', 'on')
        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)

        # 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 except
        # trac.test are enabled
        return component_name.startswith('trac.') and \
               not component_name.startswith('trac.test.') 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."""
        try:
            tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0]
            if tag != _VERSION:
                raise Exception("Unknown Trac environment type '%s'" % tag)
        except Exception as e:
            raise TracError("No Trac environment found at %s\n%s"
                            % (self.path, e))

    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:
               ...

        Note that within the block, you don't need to (and shouldn't)
        call ``commit()`` yourself, the context manager will take care
        of it (if it's the outermost such context manager on the
        stack).


        `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()

    @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()

    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
        (`~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."""
        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'), _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(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, 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
        """
        rows = self.db_query("""
                SELECT value FROM system WHERE name='%sdatabase_version'
                """ % ('initial_' if initial else ''))
        return int(rows[0][0]) if rows else False

    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
        logid = 'Trac.%s' % sha1(self.path).hexdigest()
        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, logid, 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):
        """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.
        """
        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."""
        for participant in self.setup_participants:
            args = ()
            with self.db_query as db:
                if arity(participant.environment_needs_upgrade) == 1:
                    args = (db,)
                if participant.environment_needs_upgrade(*args):
                    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 = []
        for participant in self.setup_participants:
            args = ()
            with self.db_query as db:
                if arity(participant.environment_needs_upgrade) == 1:
                    args = (db,)
                if participant.environment_needs_upgrade(*args):
                    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("%s.%s upgrading...", participant.__module__,
                          participant.__class__.__name__)
            args = ()
            with self.db_transaction as db:
                if arity(participant.upgrade_environment) == 1:
                    args = (db,)
                participant.upgrade_environment(*args)
            # Database schema may have changed, so close all connections
            DatabaseManager(self).shutdown()
        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.warn("base_url option not set in configuration, "
                          "generated links may be incorrect")
            _abs_href = Href('')
        else:
            _abs_href = Href(self.base_url)
        return _abs_href
示例#31
0
class Smileys(Component):
    """Replace smiley characters like `:-)` with icons.

    Smiley characters and icons are configurable in the `[wikiextras-smileys]`
    section in `trac.ini`. Use the `ShowSmileys` macro to display a list of
    currently defined smileys.
    """

    implements(IWikiMacroProvider, IWikiSyntaxProvider)

    smileys_section = ConfigSection(
        'wikiextras-smileys',
        """The set of smileys is configurable by providing associations
            between icon names and wiki keywords. A default set of icons and
            keywords is defined, which can be revoked one-by-one (_remove) or
            all at once (_remove_defaults).

            Example:
            {{{
            [wikiextras-smileys]
            _remove_defaults = true
            _remove = :-( :(
            smiley = :-) :)
            smiley-wink = ;-) ;)
            clock = (CLOCK) (TIME)
            calendar-month = (CALENDAR) (DATE)
            chart = (CHART)
            document-excel = (EXCEL)
            document-word = (WORD)
            eye = (EYE)
            new = (NEW)
            tick = (TICK)
            }}}

            Keywords are space-separated!

            A smiley can also be removed by associating its icon with nothing:
            {{{
            smiley =
            }}}

            Use the `ShowSmileys` macro to find out the current set of icons
            and keywords.
            """)

    remove_defaults = BoolOption('wikiextras-smileys',
                                 '_remove_defaults',
                                 False,
                                 doc="Set to true to remove all "
                                 "default smileys.")

    remove = ListOption('wikiextras-smileys',
                        '_remove',
                        sep=' ',
                        doc="""\
            Space-separated(!) list of keywords that shall not be interpreted
            as smileys (even if defined in this section).""")

    def __init__(self):
        self.smileys = None

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        if self.smileys is None:
            self.smileys = SMILEYS.copy()
            if self.remove_defaults:
                self.smileys = {}
            for icon_name, value in self.smileys_section.options():
                if not icon_name.startswith('_remove'):
                    icon_file = icon_name
                    if not icon_file.endswith('.png'):
                        icon_file = '%s.png' % icon_file
                    if value:
                        for keyword in value.split():
                            self.smileys[keyword.strip()] = icon_file
                    else:
                        # no keyword, remove all smileys associated with icon
                        for k in self.smileys.keys():
                            if self.smileys[k] == icon_file:
                                del self.smileys[k]
            for keyword in self.remove:
                if keyword in self.smileys:
                    del self.smileys[keyword]

        if self.smileys:
            yield (r"(?<!\w)!?(?:%s)" % prepare_regexp(self.smileys),
                   self._format_smiley)
        else:
            yield (None, None)

    def get_link_resolvers(self):
        return []

    #noinspection PyUnusedLocal
    def _format_smiley(self, formatter, match, fullmatch=None):
        #noinspection PyArgumentList
        loc = Icons(self.env).icon_location()
        return tag.img(src=formatter.href.chrome(loc[0], self.smileys[match]),
                       alt=match,
                       style="vertical-align: text-bottom")

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'ShowSmileys'

    #noinspection PyUnusedLocal
    def get_macro_description(self, name):
        return cleandoc("""Renders in a table the list of available smileys.
                Optional argument is the number of columns in the table
                (defaults 3).

                Comment: Prefixing a character sequence with `!` prevents it
                from being interpreted as a smiley.
                """)

    #noinspection PyUnusedLocal
    def expand_macro(self, formatter, name, content, args=None):
        # Merge smileys for presentation
        # First collect wikitexts for each unique filename
        syelims = {}  # key=filename, value=wikitext
        for wikitext, filename in self.smileys.iteritems():
            if filename not in syelims:
                syelims[filename] = [wikitext]
            else:
                syelims[filename].append(wikitext)
        # Reverse
        smileys = {}
        for filename, wikitexts in syelims.iteritems():
            wikitexts.sort()
            smileys[' '.join(wikitexts)] = filename
        return render_table(
            smileys.keys(), content,
            lambda s: self._format_smiley(formatter,
                                          s.split(' ', 1)[0]))
示例#32
0
文件: api.py 项目: t2y/trac
class RepositoryManager(Component):
    """Version control system manager."""

    implements(IRequestFilter, IResourceManager, IRepositoryProvider)

    connectors = ExtensionPoint(IRepositoryConnector)
    providers = ExtensionPoint(IRepositoryProvider)
    change_listeners = ExtensionPoint(IRepositoryChangeListener)

    repositories_section = ConfigSection(
        'repositories',
        """One of the alternatives for registering new repositories is to
        populate the `[repositories]` section of the `trac.ini`.

        This is especially suited for setting up convenience aliases,
        short-lived repositories, or during the initial phases of an
        installation.

        See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details
        about the format adopted for this section and the rest of that page for
        the other alternatives.

        (''since 0.12'')""")

    repository_type = Option(
        'trac', 'repository_type', 'svn',
        """Default repository connector type. (''since 0.10'')

        This is also used as the default repository type for repositories
        defined in [[TracIni#repositories-section repositories]] or using the
        "Repositories" admin panel. (''since 0.12'')
        """)

    repository_dir = Option(
        'trac', 'repository_dir', '',
        """Path to the default repository. This can also be a relative path
        (''since 0.11'').

        This option is deprecated, and repositories should be defined in the
        [TracIni#repositories-section repositories] section, or using the
        "Repositories" admin panel. (''since 0.12'')""")

    repository_sync_per_request = ListOption(
        'trac',
        'repository_sync_per_request',
        '(default)',
        doc="""List of repositories that should be synchronized on every page
        request.

        Leave this option empty if you have set up post-commit hooks calling
        `trac-admin $ENV changeset added` on all your repositories
        (recommended). Otherwise, set it to a comma-separated list of
        repository names. Note that this will negatively affect performance,
        and will prevent changeset listeners from receiving events from the
        repositories specified here. (''since 0.12'')""")

    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()
        self._connectors = None
        self._all_repositories = None

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        from trac.web.chrome import Chrome, add_warning
        if handler is not Chrome(self.env):
            for reponame in self.repository_sync_per_request:
                start = time.time()
                if is_default(reponame):
                    reponame = ''
                try:
                    repo = self.get_repository(reponame)
                    if repo:
                        repo.sync()
                    else:
                        self.log.warning(
                            "Unable to find repository '%s' for "
                            "synchronization", reponame or '(default)')
                        continue
                except TracError as e:
                    add_warning(
                        req,
                        _(
                            "Can't synchronize with repository \"%(name)s\" "
                            "(%(error)s). Look in the Trac log for more "
                            "information.",
                            name=reponame or '(default)',
                            error=to_unicode(e)))
                except Exception as e:
                    add_warning(
                        req,
                        _(
                            "Failed to sync with repository \"%(name)s\": "
                            "%(error)s; repository information may be out of "
                            "date. Look in the Trac log for more information "
                            "including mitigation strategies.",
                            name=reponame or '(default)',
                            error=to_unicode(e)))
                    self.log.error(
                        "Failed to sync with repository \"%s\"; You may be "
                        "able to reduce the impact of this issue by "
                        "configuring [trac] repository_sync_per_request; see "
                        "http://trac.edgewall.org/wiki/TracRepositoryAdmin"
                        "#ExplicitSync for more detail: %s",
                        reponame or '(default)',
                        exception_to_unicode(e, traceback=True))
                self.log.info("Synchronized '%s' repository in %0.2f seconds",
                              reponame or '(default)',
                              time.time() - start)
        return handler

    def post_process_request(self, req, template, data, content_type):
        return (template, data, content_type)

    # IResourceManager methods

    def get_resource_realms(self):
        yield 'changeset'
        yield 'source'
        yield 'repository'

    def get_resource_description(self, resource, format=None, **kwargs):
        if resource.realm == 'changeset':
            parent = resource.parent
            reponame = parent and parent.id
            id = resource.id
            if reponame:
                return _("Changeset %(rev)s in %(repo)s",
                         rev=id,
                         repo=reponame)
            else:
                return _("Changeset %(rev)s", rev=id)
        elif resource.realm == 'source':
            parent = resource.parent
            reponame = parent and parent.id
            id = resource.id
            version = ''
            if format == 'summary':
                repos = self.get_repository(reponame)
                node = repos.get_node(resource.id, resource.version)
                if node.isdir:
                    kind = _("directory")
                elif node.isfile:
                    kind = _("file")
                if resource.version:
                    version = _(" at version %(rev)s", rev=resource.version)
            else:
                kind = _("path")
                if resource.version:
                    version = '@%s' % resource.version
            in_repo = _(" in %(repo)s", repo=reponame) if reponame else ''
            # TRANSLATOR: file /path/to/file.py at version 13 in reponame
            return _('%(kind)s %(id)s%(at_version)s%(in_repo)s',
                     kind=kind,
                     id=id,
                     at_version=version,
                     in_repo=in_repo)
        elif resource.realm == 'repository':
            if not resource.id:
                return _("Default repository")
            return _("Repository %(repo)s", repo=resource.id)

    def get_resource_url(self, resource, href, **kwargs):
        if resource.realm == 'changeset':
            parent = resource.parent
            return href.changeset(resource.id, parent and parent.id or None)
        elif resource.realm == 'source':
            parent = resource.parent
            return href.browser(parent and parent.id or None,
                                resource.id,
                                rev=resource.version or None)
        elif resource.realm == 'repository':
            return href.browser(resource.id or None)

    def resource_exists(self, resource):
        if resource.realm == 'repository':
            reponame = resource.id
        else:
            reponame = resource.parent.id
        repos = self.env.get_repository(reponame)
        if not repos:
            return False
        if resource.realm == 'changeset':
            try:
                repos.get_changeset(resource.id)
                return True
            except NoSuchChangeset:
                return False
        elif resource.realm == 'source':
            try:
                repos.get_node(resource.id, resource.version)
                return True
            except NoSuchNode:
                return False
        elif resource.realm == 'repository':
            return True

    # IRepositoryProvider methods

    def get_repositories(self):
        """Retrieve repositories specified in TracIni.

        The `[repositories]` section can be used to specify a list
        of repositories.
        """
        repositories = self.repositories_section
        reponames = {}
        # eventually add pre-0.12 default repository
        if self.repository_dir:
            reponames[''] = {'dir': self.repository_dir}
        # first pass to gather the <name>.dir entries
        for option in repositories:
            if option.endswith('.dir'):
                reponames[option[:-4]] = {}
        # second pass to gather aliases
        for option in repositories:
            alias = repositories.get(option)
            if '.' not in option:  # Support <alias> = <repo> syntax
                option += '.alias'
            if option.endswith('.alias') and alias in reponames:
                reponames.setdefault(option[:-6], {})['alias'] = alias
        # third pass to gather the <name>.<detail> entries
        for option in repositories:
            if '.' in option:
                name, detail = option.rsplit('.', 1)
                if name in reponames and detail != 'alias':
                    reponames[name][detail] = repositories.get(option)

        for reponame, info in reponames.iteritems():
            yield (reponame, info)

    # Public API methods

    def get_supported_types(self):
        """Return the list of supported repository types."""
        types = set(type_ for connector in self.connectors
                    for (type_, prio) in connector.get_supported_types() or []
                    if prio >= 0)
        return list(types)

    def get_repositories_by_dir(self, directory):
        """Retrieve the repositories based on the given directory.

           :param directory: the key for identifying the repositories.
           :return: list of `Repository` instances.
        """
        directory = os.path.join(os.path.normcase(directory), '')
        repositories = []
        for reponame, repoinfo in self.get_all_repositories().iteritems():
            dir = repoinfo.get('dir')
            if dir:
                dir = os.path.join(os.path.normcase(dir), '')
                if dir.startswith(directory):
                    repos = self.get_repository(reponame)
                    if repos:
                        repositories.append(repos)
        return repositories

    def get_repository_id(self, reponame):
        """Return a unique id for the given repository name.

        This will create and save a new id if none is found.

        Note: this should probably be renamed as we're dealing
              exclusively with *db* repository ids here.
        """
        with self.env.db_transaction as db:
            for id, in db(
                    "SELECT id FROM repository WHERE name='name' AND value=%s",
                (reponame, )):
                return id

            id = db("SELECT COALESCE(MAX(id), 0) FROM repository")[0][0] + 1
            db("INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
               (id, 'name', reponame))
            return id

    def get_repository(self, reponame):
        """Retrieve the appropriate `Repository` for the given
        repository name.

           :param reponame: the key for specifying the repository.
                            If no name is given, take the default
                            repository.
           :return: if no corresponding repository was defined,
                    simply return `None`.
        """
        reponame = reponame or ''
        repoinfo = self.get_all_repositories().get(reponame, {})
        if 'alias' in repoinfo:
            reponame = repoinfo['alias']
            repoinfo = self.get_all_repositories().get(reponame, {})
        rdir = repoinfo.get('dir')
        if not rdir:
            return None
        rtype = repoinfo.get('type') or self.repository_type

        # get a Repository for the reponame (use a thread-level cache)
        with self.env.db_transaction:  # prevent possible deadlock, see #4465
            with self._lock:
                tid = threading._get_ident()
                if tid in self._cache:
                    repositories = self._cache[tid]
                else:
                    repositories = self._cache[tid] = {}
                repos = repositories.get(reponame)
                if not repos:
                    if not os.path.isabs(rdir):
                        rdir = os.path.join(self.env.path, rdir)
                    connector = self._get_connector(rtype)
                    repos = connector.get_repository(rtype, rdir,
                                                     repoinfo.copy())
                    repositories[reponame] = repos
                return repos

    def get_repository_by_path(self, path):
        """Retrieve a matching `Repository` for the given `path`.

        :param path: the eventually scoped repository-scoped path
        :return: a `(reponame, repos, path)` triple, where `path` is
                 the remaining part of `path` once the `reponame` has
                 been truncated, if needed.
        """
        matches = []
        path = path.strip('/') + '/' if path else '/'
        for reponame in self.get_all_repositories().keys():
            stripped_reponame = reponame.strip('/') + '/'
            if path.startswith(stripped_reponame):
                matches.append((len(stripped_reponame), reponame))
        if matches:
            matches.sort()
            length, reponame = matches[-1]
            path = path[length:]
        else:
            reponame = ''
        return (reponame, self.get_repository(reponame), path.rstrip('/')
                or '/')

    def get_default_repository(self, context):
        """Recover the appropriate repository from the current context.

        Lookup the closest source or changeset resource in the context
        hierarchy and return the name of its associated repository.
        """
        while context:
            if context.resource.realm in ('source', 'changeset'):
                return context.resource.parent.id
            context = context.parent

    def get_all_repositories(self):
        """Return a dictionary of repository information, indexed by name."""
        if not self._all_repositories:
            all_repositories = {}
            for provider in self.providers:
                for reponame, info in provider.get_repositories() or []:
                    if reponame in all_repositories:
                        self.log.warn("Discarding duplicate repository '%s'",
                                      reponame)
                    else:
                        info['name'] = reponame
                        if 'id' not in info:
                            info['id'] = self.get_repository_id(reponame)
                        all_repositories[reponame] = info
            self._all_repositories = all_repositories
        return self._all_repositories

    def get_real_repositories(self):
        """Return a set of all real repositories (i.e. excluding aliases)."""
        repositories = set()
        for reponame in self.get_all_repositories():
            try:
                repos = self.get_repository(reponame)
                if repos is not None:
                    repositories.add(repos)
            except TracError:
                pass  # Skip invalid repositories
        return repositories

    def reload_repositories(self):
        """Reload the repositories from the providers."""
        with self._lock:
            # FIXME: trac-admin doesn't reload the environment
            self._cache = {}
            self._all_repositories = None
        self.config.touch()  # Force environment reload

    def notify(self, event, reponame, revs):
        """Notify repositories and change listeners about repository events.

        The supported events are the names of the methods defined in the
        `IRepositoryChangeListener` interface.
        """
        self.log.debug("Event %s on repository '%s' for changesets %r", event,
                       reponame or '(default)', revs)

        # Notify a repository by name, and all repositories with the same
        # base, or all repositories by base or by repository dir
        repos = self.get_repository(reponame)
        repositories = []
        if repos:
            base = repos.get_base()
        else:
            dir = os.path.abspath(reponame)
            repositories = self.get_repositories_by_dir(dir)
            if repositories:
                base = None
            else:
                base = reponame
        if base:
            repositories = [
                r for r in self.get_real_repositories() if r.get_base() == base
            ]
        if not repositories:
            self.log.warn("Found no repositories matching '%s' base.", base
                          or reponame)
            return
        for repos in sorted(repositories, key=lambda r: r.reponame):
            repos.sync()
            for rev in revs:
                args = []
                if event == 'changeset_modified':
                    args.append(repos.sync_changeset(rev))
                try:
                    changeset = repos.get_changeset(rev)
                except NoSuchChangeset:
                    try:
                        repos.sync_changeset(rev)
                        changeset = repos.get_changeset(rev)
                    except NoSuchChangeset:
                        self.log.debug(
                            "No changeset '%s' found in repository '%s'. "
                            "Skipping subscribers for event %s", rev,
                            repos.reponame or '(default)', event)
                        continue
                self.log.debug("Event %s on repository '%s' for revision '%s'",
                               event, repos.reponame or '(default)', rev)
                for listener in self.change_listeners:
                    getattr(listener, event)(repos, changeset, *args)

    def shutdown(self, tid=None):
        """Free `Repository` instances bound to a given thread identifier"""
        if tid:
            assert tid == threading._get_ident()
            with self._lock:
                repositories = self._cache.pop(tid, {})
                for reponame, repos in repositories.iteritems():
                    repos.close()

    # private methods

    def _get_connector(self, rtype):
        """Retrieve the appropriate connector for the given repository type.

        Note that the self._lock must be held when calling this method.
        """
        if self._connectors is None:
            # build an environment-level cache for the preferred connectors
            self._connectors = {}
            for connector in self.connectors:
                for type_, prio in connector.get_supported_types() or []:
                    keep = (connector, prio)
                    if type_ in self._connectors and \
                            prio <= self._connectors[type_][1]:
                        keep = None
                    if keep:
                        self._connectors[type_] = keep
        if rtype in self._connectors:
            connector, prio = self._connectors[rtype]
            if prio >= 0:  # no error condition
                return connector
            else:
                raise TracError(
                    _(
                        'Unsupported version control system "%(name)s"'
                        ': %(error)s',
                        name=rtype,
                        error=to_unicode(connector.error)))
        else:
            raise TracError(
                _(
                    'Unsupported version control system "%(name)s": '
                    'Can\'t find an appropriate component, maybe the '
                    'corresponding plugin was not enabled? ',
                    name=rtype))
示例#33
0
文件: main.py 项目: wataash/trac
class RequestDispatcher(Component):
    """Web request dispatcher.

    This component dispatches incoming requests to registered handlers.
    It also takes care of user authentication and request pre- and
    post-processing.
    """
    required = True

    implements(ITemplateProvider)

    authenticators = ExtensionPoint(IAuthenticator)
    handlers = ExtensionPoint(IRequestHandler)

    filters = OrderedExtensionsOption('trac', 'request_filters',
                                      IRequestFilter,
        doc="""Ordered list of filters to apply to all requests.""")

    default_handler = ExtensionOption('trac', 'default_handler',
                                      IRequestHandler, 'WikiModule',
        """Name of the component that handles requests to the base
        URL.

        Options include `TimelineModule`, `RoadmapModule`,
        `BrowserModule`, `QueryModule`, `ReportModule`, `TicketModule`
        and `WikiModule`.

        The [/prefs/userinterface session preference] for default handler
        take precedence, when set.
        """)

    default_timezone = Option('trac', 'default_timezone', '',
        """The default timezone to use""")

    default_language = Option('trac', 'default_language', '',
        """The preferred language to use if no user preference has been set.
        """)

    default_date_format = ChoiceOption('trac', 'default_date_format',
                                       ('', 'iso8601'),
        """The date format. Valid options are 'iso8601' for selecting
        ISO 8601 format, or leave it empty which means the default
        date format will be inferred from the browser's default
        language. (''since 1.0'')
        """)

    use_xsendfile = BoolOption('trac', 'use_xsendfile', 'false',
        """When true, send a `X-Sendfile` header and no content when sending
        files from the filesystem, so that the web server handles the content.
        This requires a web server that knows how to handle such a header,
        like Apache with `mod_xsendfile` or lighttpd. (''since 1.0'')
        """)

    xsendfile_header = Option('trac', 'xsendfile_header', 'X-Sendfile',
        """The header to use if `use_xsendfile` is enabled. If Nginx is used,
        set `X-Accel-Redirect`. (''since 1.0.6'')""")

    configurable_headers = ConfigSection('http-headers', """
        Headers to be added to the HTTP request. (''since 1.2.3'')

        The header name must conform to RFC7230 and the following
        reserved names are not allowed: content-type, content-length,
        location, etag, pragma, cache-control, expires.
        """)

    # Public API

    def authenticate(self, req):
        for authenticator in self.authenticators:
            try:
                authname = authenticator.authenticate(req)
            except TracError as e:
                self.log.error("Can't authenticate using %s: %s",
                               authenticator.__class__.__name__,
                               exception_to_unicode(e, traceback=True))
                add_warning(req, _("Authentication error. "
                                   "Please contact your administrator."))
                break  # don't fallback to other authenticators
            if authname:
                return authname
        return 'anonymous'

    def dispatch(self, req):
        """Find a registered handler that matches the request and let
        it process it.

        In addition, this method initializes the data dictionary
        passed to the the template and adds the web site chrome.
        """
        self.log.debug('Dispatching %r', req)
        chrome = Chrome(self.env)

        try:
            # Select the component that should handle the request
            chosen_handler = None
            for handler in self._request_handlers.values():
                if handler.match_request(req):
                    chosen_handler = handler
                    break
            if not chosen_handler and req.path_info in ('', '/'):
                chosen_handler = self._get_valid_default_handler(req)
            # pre-process any incoming request, whether a handler
            # was found or not
            self.log.debug("Chosen handler is %s", chosen_handler)
            chosen_handler = self._pre_process_request(req, chosen_handler)
            if not chosen_handler:
                if req.path_info.endswith('/'):
                    # Strip trailing / and redirect
                    target = unicode_quote(req.path_info.rstrip('/'))
                    if req.query_string:
                        target += '?' + req.query_string
                    req.redirect(req.href + target, permanent=True)
                raise HTTPNotFound('No handler matched request to %s',
                                   req.path_info)

            req.callbacks['chrome'] = partial(chrome.prepare_request,
                                              handler=chosen_handler)

            # Protect against CSRF attacks: we validate the form token
            # for all POST requests with a content-type corresponding
            # to form submissions
            if req.method == 'POST':
                ctype = req.get_header('Content-Type')
                if ctype:
                    ctype, options = cgi.parse_header(ctype)
                if ctype in ('application/x-www-form-urlencoded',
                             'multipart/form-data') and \
                        req.args.get('__FORM_TOKEN') != req.form_token:
                    if self.env.secure_cookies and req.scheme == 'http':
                        msg = _('Secure cookies are enabled, you must '
                                'use https to submit forms.')
                    else:
                        msg = _('Do you have cookies enabled?')
                    raise HTTPBadRequest(_('Missing or invalid form token.'
                                           ' %(msg)s', msg=msg))

            # Process the request and render the template
            resp = chosen_handler.process_request(req)
            if resp:
                template, data, metadata = \
                    self._post_process_request(req, *resp)
                if 'hdfdump' in req.args:
                    req.perm.require('TRAC_ADMIN')
                    # debugging helper - no need to render first
                    out = io.BytesIO()
                    pprint({'template': template,
                            'metadata': metadata,
                            'data': data}, out)
                    req.send(out.getvalue(), 'text/plain')
                self.log.debug("Rendering response with template %s", template)
                metadata.setdefault('iterable', chrome.use_chunked_encoding)
                content_type = metadata.get('content_type')
                output = chrome.render_template(req, template, data, metadata)
                req.send(output, content_type or 'text/html')
            else:
                self.log.debug("Empty or no response from handler. "
                               "Entering post_process_request.")
                self._post_process_request(req)
        except RequestDone:
            raise
        except Exception as e:
            # post-process the request in case of errors
            err = sys.exc_info()
            try:
                self._post_process_request(req)
            except RequestDone:
                raise
            except TracError as e2:
                self.log.warning("Exception caught while post-processing"
                                 " request: %s", exception_to_unicode(e2))
            except Exception as e2:
                if not (type(e) is type(e2) and e.args == e2.args):
                    self.log.error("Exception caught while post-processing"
                                   " request: %s",
                                   exception_to_unicode(e2, traceback=True))
            if isinstance(e, PermissionError):
                raise HTTPForbidden(e)
            if isinstance(e, ResourceNotFound):
                raise HTTPNotFound(e)
            if isinstance(e, NotImplementedError):
                tb = traceback.extract_tb(err[2])[-1]
                self.log.warning("%s caught from %s:%d in %s: %s",
                                 e.__class__.__name__, tb[0], tb[1], tb[2],
                                 to_unicode(e) or "(no message)")
                raise HTTPInternalServerError(TracNotImplementedError(e))
            if isinstance(e, TracError):
                raise HTTPInternalServerError(e)
            raise err[0], err[1], err[2]

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        return [pkg_resources.resource_filename('trac.web', 'templates')]

    # Internal methods

    def set_default_callbacks(self, req):
        """Setup request callbacks for lazily-evaluated properties.
        """
        req.callbacks.update({
            'authname': self.authenticate,
            'chrome': Chrome(self.env).prepare_request,
            'form_token': self._get_form_token,
            'lc_time': self._get_lc_time,
            'locale': self._get_locale,
            'perm': self._get_perm,
            'session': self._get_session,
            'tz': self._get_timezone,
            'use_xsendfile': self._get_use_xsendfile,
            'xsendfile_header': self._get_xsendfile_header,
            'configurable_headers': self._get_configurable_headers,
        })

    @lazy
    def _request_handlers(self):
        return {handler.__class__.__name__: handler
                for handler in self.handlers}

    def _get_valid_default_handler(self, req):
        # Use default_handler from the Session if it is a valid value.
        name = req.session.get('default_handler')
        handler = self._request_handlers.get(name)
        if handler and not is_valid_default_handler(handler):
            handler = None

        if not handler:
            # Use default_handler from project configuration.
            handler = self.default_handler
            if not is_valid_default_handler(handler):
                raise ConfigurationError(
                    tag_("%(handler)s is not a valid default handler. Please "
                         "update %(option)s through the %(page)s page or by "
                         "directly editing trac.ini.",
                         handler=tag.code(handler.__class__.__name__),
                         option=tag.code("[trac] default_handler"),
                         page=tag.a(_("Basic Settings"),
                                    href=req.href.admin('general/basics'))))
        return handler

    def _get_perm(self, req):
        if isinstance(req.session, FakeSession):
            return FakePerm()
        else:
            return PermissionCache(self.env, req.authname)

    def _get_session(self, req):
        try:
            return Session(self.env, req)
        except TracError as e:
            msg = "can't retrieve session: %s"
            if isinstance(e, TracValueError):
                self.log.warning(msg, e)
            else:
                self.log.error(msg, exception_to_unicode(e))
            return FakeSession()

    def _get_locale(self, req):
        if has_babel:
            preferred = req.session.get('language')
            default = self.default_language
            negotiated = get_negotiated_locale([preferred, default] +
                                               req.languages)
            self.log.debug("Negotiated locale: %s -> %s", preferred,
                           negotiated)
            return negotiated

    def _get_lc_time(self, req):
        lc_time = req.session.get('lc_time')
        if not lc_time or lc_time == 'locale' and not has_babel:
            lc_time = self.default_date_format
        if lc_time == 'iso8601':
            return 'iso8601'
        return req.locale

    def _get_timezone(self, req):
        try:
            return timezone(req.session.get('tz', self.default_timezone
                                            or 'missing'))
        except Exception:
            return localtz

    def _get_form_token(self, req):
        """Used to protect against CSRF.

        The 'form_token' is strong shared secret stored in a user
        cookie. By requiring that every POST form to contain this
        value we're able to protect against CSRF attacks. Since this
        value is only known by the user and not by an attacker.

        If the the user does not have a `trac_form_token` cookie a new
        one is generated.
        """
        if 'trac_form_token' in req.incookie:
            return req.incookie['trac_form_token'].value
        else:
            req.outcookie['trac_form_token'] = form_token = hex_entropy(24)
            req.outcookie['trac_form_token']['path'] = req.base_path or '/'
            if self.env.secure_cookies:
                req.outcookie['trac_form_token']['secure'] = True
            req.outcookie['trac_form_token']['httponly'] = True
            return form_token

    def _get_use_xsendfile(self, req):
        return self.use_xsendfile

    @lazy
    def _xsendfile_header(self):
        header = self.xsendfile_header.strip()
        if Request.is_valid_header(header):
            return to_utf8(header)
        else:
            if not self._warn_xsendfile_header:
                self._warn_xsendfile_header = True
                self.log.warning("[trac] xsendfile_header is invalid: '%s'",
                                 header)
            return None

    def _get_xsendfile_header(self, req):
        return self._xsendfile_header

    @lazy
    def _configurable_headers(self):
        headers = []
        invalids = []
        for name, val in self.configurable_headers.options():
            if Request.is_valid_header(name, val):
                headers.append((name, val))
            else:
                invalids.append((name, val))
        if invalids:
            self.log.warning('[http-headers] invalid headers are ignored: %s',
                             ', '.join('%r: %r' % i for i in invalids))
        return tuple(headers)

    def _get_configurable_headers(self, req):
        return iter(self._configurable_headers)

    def _pre_process_request(self, req, chosen_handler):
        for filter_ in self.filters:
            chosen_handler = filter_.pre_process_request(req, chosen_handler)
        return chosen_handler

    def _post_process_request(self, req, *args):
        metadata = {}
        resp = args
        if len(args) == 3:
            metadata = args[2]
        elif len(args) == 2:
            resp += (metadata,)
        elif len(args) == 0:
            resp = (None,) * 3
        for f in reversed(self.filters):
            resp = f.post_process_request(req, *resp)
            if len(resp) == 2:
                resp += (metadata,)
        return resp
示例#34
0
class Symbols(Component):
    """Replace character sequences with symbols.

    Characters and symbols are configurable in the `[wikiextras-symbols]`
    section in `trac.ini`. Use the `ShowSymbols` macro to display a list of
    currently defined symbols.
    """

    implements(IWikiMacroProvider, IWikiSyntaxProvider)

    symbols_section = ConfigSection(
        'wikiextras-symbols',
        """The set of symbols is configurable by providing associations
            between symbols and wiki keywords. A default set of symbols and
            keywords is defined, which can be revoked one-by-one (_remove) or
            all at once (_remove_defaults).

            Example:
            {{{
            [wikiextras-symbols]
            _remove_defaults = true
            _remove = <- ->
            &laquo; = <<
            &raquo; = >>
            &sum; = (SUM)
            &hearts; = <3
            }}}

            Keywords are space-separated!

            A symbol can also be removed by associating it with no keyword:
            {{{
            &larr; =
            }}}

            Use the `ShowSymbols` macro to find out the current set of symbols
            and keywords.
            """)

    remove_defaults = BoolOption('wikiextras-symbols',
                                 '_remove_defaults',
                                 False,
                                 doc="Set to true to remove all "
                                 "default symbols.")

    remove = ListOption('wikiextras-symbols',
                        '_remove',
                        sep=' ',
                        doc="""\
            Space-separated(!) list of keywords that shall not be interpreted
            as symbols (even if defined in this section).""")

    def __init__(self):
        self.symbols = None

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        if self.symbols is None:
            self.symbols = SYMBOLS.copy()
            if self.remove_defaults:
                self.symbols = {}
            for symbol, value in self.symbols_section.options():
                if not symbol.startswith('_remove'):
                    if value:
                        for keyword in value.split():
                            self.symbols[keyword.strip()] = symbol
                    else:
                        # no keyword, remove all keywords associated with
                        # symbol
                        for k in self.symbols.keys():
                            if self.symbols[k] == symbol:
                                del self.symbols[k]
            for keyword in self.remove:
                if keyword in self.symbols:
                    del self.symbols[keyword]

        if self.symbols:
            yield ('!?%s' % prepare_regexp(self.symbols), self._format_symbol)
        else:
            yield (None, None)

    def get_link_resolvers(self):
        return []

    #noinspection PyUnusedLocal
    def _format_symbol(self, formatter, match, fullmatch):
        return Markup(self.symbols[match])

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'ShowSymbols'

    #noinspection PyUnusedLocal
    def get_macro_description(self, name):
        return ("Renders in a table the list of known symbols. "
                "Optional argument is the number of columns in the table "
                "(defaults 3).")

    #noinspection PyUnusedLocal
    def expand_macro(self, formatter, name, content, args=None):
        return render_table(self.symbols.keys(),
                            content,
                            lambda s: self._format_symbol(formatter, s, None),
                            colspace=4)