示例#1
0
class QueuesAjaxModule(Component):
    implements(IRequestHandler)

    audit = ChoiceOption(
        'queues',
        'audit',
        choices=['log', 'ticket', 'none'],
        doc="Record reorderings in log, in ticket, or not at all.")

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/queuesajax')

    def process_request(self, req):
        """Process AJAX request.  Args come back in this form:
        
          id5=position1&id23=position2
        
        where the name is the ticket id prefixed with 'id' and the
        value is the new position value prefixed with the name of
        the first column in the report.
        
        IMPORTANT: DO NOT RENAME THE FIRST COLUMN IN THE REPORT!
        This code assumes that the name of the first column in the
        report exactly matches the ticket's field name.  This is
        to allow any position field name versus hard-coding it.
        """
        try:
            changes = self._get_changes(req.args)
            self._save_changes(changes, req.authname)
            code, msg = 200, "OK"
        except Exception, e:
            import traceback
            code, msg = 500, "Oops...\n" + traceback.format_exc() + "\n"
        req.send_response(code)
        req.send_header('Content-Type', 'text/plain')
        req.send_header('Content-Length', len(msg))
        req.end_headers()
        req.write(msg)
示例#2
0
class TracWorkflowAdminModule(Component):
    implements(IAdminPanelProvider, ITemplateProvider,
               IEnvironmentSetupParticipant)

    operations = ListOption('workflow-admin', 'operations',
        'del_owner, set_owner, set_owner_to_self, del_resolution, '
        'set_resolution, leave_status',
        doc=N_("Operations in workflow admin"))
    dot_path = Option('workflow-admin', 'dot_path', 'dot',
        doc=N_("Path to the dot executable"))
    diagram_cache = BoolOption('workflow-admin', 'diagram_cache', 'false',
        doc=N_("Enable cache of workflow diagram image"))
    diagram_size = Option('workflow-admin', 'diagram_size', '6, 6',
        doc=N_("Image size in workflow diagram"))
    diagram_font = Option('workflow-admin', 'diagram_font', 'sans-serif',
        doc=N_("Font name in workflow diagram"))
    diagram_fontsize = FloatOption('workflow-admin', 'diagram_fontsize', '10',
        doc=N_("Font size in workflow diagram"))
    diagram_colors = ListOption('workflow-admin', 'diagram_colors',
        '#0000ff, #006600, #ff0000, #666600, #ff00ff',
        doc=N_("Colors of arrows in workflow diagram"))
    default_editor = ChoiceOption(
        'workflow-admin', 'default_editor', ['gui', 'text'],
        doc=N_("Default mode of the workflow editor"))
    auto_update_interval = Option(
        'workflow-admin', 'auto_update_interval', '3000',
        doc=N_("An automatic-updating interval for text mode is specified by "
               "a milli second bit. It is not performed when 0 is specified."))

    msgjs_locales = _msgjs_locales()
    _action_name_re = re.compile(r'\A[A-Za-z0-9_-]+\Z')
    _number_re = re.compile(r'\A[0-9]+\Z')

    def __init__(self):
        locale_dir = resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)

    # IEnvironmentSetupParticipant
    def environment_created(self):
        pass

    def environment_needs_upgrade(self, db):
        return False

    def upgrade_environment(self, db):
        pass

    # ITemplateProvider method
    def get_htdocs_dirs(self):
        return [('tracworkflowadmin', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return [resource_filename(__name__, 'templates')]

    # IAdminPanelProvider methods
    def get_admin_panels(self, req):
        if 'TRAC_ADMIN' in req.perm:
            yield ('ticket', dgettext("messages", ("Ticket System")),
                   'workflowadmin', _("Workflow Admin"))

    def render_admin_panel(self, req, cat, page, path_info):
        req.perm.assert_permission('TRAC_ADMIN')
        if req.method == 'POST':
            self._parse_request(req)

        action, status = self._conf_to_inner_format(self.config)
        operations = self.operations
        permissions = self._get_permissions(req)
        add_stylesheet(req, 'tracworkflowadmin/themes/base/jquery-ui.css')
        add_stylesheet(req, 'tracworkflowadmin/css/tracworkflowadmin.css')
        add_stylesheet(req, 'tracworkflowadmin/css/jquery.multiselect.css')
        add_script(req, 'tracworkflowadmin/scripts/jquery-ui.js')
        add_script(req, 'tracworkflowadmin/scripts/jquery.json-2.2.js')
        add_script(req, 'tracworkflowadmin/scripts/jquery.multiselect.js')
        add_script(req, 'tracworkflowadmin/scripts/main.js')
        add_script_data(req, {'auto_update_interval': int(self.auto_update_interval)})
        if req.locale and str(req.locale) in self.msgjs_locales:
            add_script(req, 'tracworkflowadmin/scripts/messages/%s.js' % req.locale)
        data = {
            'actions': action,
            'status': status,
            'perms': permissions,
            'operations': operations,
            'editor_mode': req.args.get('editor_mode') or self.default_editor,
            'text': self._conf_to_str(self.config)
        }
        return 'tracworkflowadmin.html', data

    def _conf_to_inner_format(self, conf):
        statuses = []
        for name, value in conf.options('ticket-workflow'):
            if name.endswith('.operations') and 'leave_status' in [before.strip() for before in value.split(',')]:
                values = conf.get('ticket-workflow', name[0:-11]).split('->')
                if values[1].strip() == '*':
                    for name in values[0].split(','):
                        st = name.strip()
                        if st != '*':
                            statuses.append(st)
                    break
        actions = {}

        count = 1
        for name, value in conf.options('ticket-workflow'):
            param = name.split('.')
            actionName = param[0].strip()
            regValue = ''
            if len(param) == 1:
                pieces = [val.strip() for val in value.split('->')]
                before = pieces[0]
                next = '*'
                if len(pieces) > 1:
                    next = pieces[1]
                regValue = {'next': next, 'before': {}}
                if next != '*' and next not in statuses:
                        statuses.append(next)
                if before != '*':
                    for val in before.split(','):
                        tmp = val.strip()
                        if tmp != '':
                            regValue['before'][tmp] = 1
                            if tmp != '*' and tmp not in statuses:
                                statuses.append(tmp)
                else:
                    regValue['before'] = '*'
                if not actions.has_key(actionName):
                    actions[actionName] = {'tempName': actionName, 'lineInfo': {}}
                actions[actionName]['next'] = regValue['next']
                actions[actionName]['before'] = regValue['before']
            else:
                regKey = param[1].strip()
                if regKey == 'permissions' or regKey == 'operations':
                    tmp = []
                    for v in value.strip().split(','):
                        tmp2 = v.strip()
                        if  tmp2 != '':
                            tmp.append(v.strip())
                    regValue = tmp
                else:
                    regValue = value.strip()
                if not actions.has_key(actionName):
                    actions[actionName] = {'tempName': actionName, 'lineInfo': {}}
                actions[actionName][regKey] = regValue
            count = count + 1

        action_elements = []
        for key in actions:
            tmp = actions[key]
            tmp['action'] = key
            if not tmp.has_key('default'):
                tmp['default'] = 0
            elif not self._number_re.match(tmp['default']):
                tmp['default'] = -1
            if not tmp.has_key('permissions'):
                tmp['permissions'] = ['All Users']
            if not tmp.has_key('name'):
                tmp['name'] = ''
            if tmp.has_key('before') and tmp['before'] == '*':
                tmp['before'] = {}
                for st in statuses:
                    tmp['before'][st] = 1
            action_elements.append(tmp)
        action_elements.sort(key=lambda v: int(v['default']), reverse=True)
        return (action_elements, statuses)

    def _conf_to_str(self, conf):
        tmp = ConfigParser.ConfigParser()
        tmp.add_section('ticket-workflow')
        for name, value in conf.options('ticket-workflow'):
            tmp.set('ticket-workflow', name.encode('utf-8'), value.encode('utf-8'))

        f = StringIO()
        tmp.write(f)
        f.flush()
        f.seek(0)
        lines = [line.decode('utf-8') for line in f
                                      if not line.startswith('[')]
        lines.sort()
        return ''.join(lines)

    def _str_to_inner_format(self, str, out):
        lines = str.splitlines(False)
        errors = []
        lineInfo = {}
        firstLineInfo = {}  # dict of (action, lineno)
        others = {}
        for idx, line in enumerate(lines):
            lineno = idx + 1
            line = line.strip()
            lines[idx] = line
            if not line or line.startswith('#') or line.startswith(';'):
                continue
            if line.startswith('['):
                errors.append(_("Line %(num)d: Could not use section.",
                                num=lineno))
                continue
            if '=' not in line:
                errors.append(_(
                    "Line %(num)d: This line is not pair of key and value.",
                    num=lineno))
                continue
            key, value = line.split('=', 1)
            key = key.strip().lower()
            value = value.strip()
            if key in lineInfo:
                errors.append(_(
                    "Line %(num)d: There is a same key in line %(num2)d.",
                    num=lineno, num2=lineInfo[key]))
                continue
            lineInfo[key] = lineno
            keys = key.split('.', 1)
            firstLineInfo.setdefault(keys[0], lineno)
            if len(keys) == 1:
                if '->' not in value:
                    errors.append(_(
                        "Line %(num)d: Must be \"<action> = <status-list> -> "
                        "<new-status>\" format.", num=lineno))
                    continue
                stats = [stat.strip()
                         for stat in value.split('->')[0].split(',')]
                for n, stat in enumerate(stats):
                    if not stat:
                        errors.append(_(
                            "Line %(num)d: #%(n)d status is empty.",
                            num=lineno, n=n + 1))
            else:
                attr = keys[1]
                if '.' in attr:
                    errors.append(_(
                        "Line %(num)d: Must be \"<action>.<attribute> = "
                        "<value>\" format.", num=lineno))
                    continue
                if attr not in ('default', 'name', 'operations', 'permissions'):
                    others.setdefault(keys[0], {})
                    others[keys[0]][attr] = value

        if not firstLineInfo:
            errors.append(_("There is no valid description."))

        actions = sorted(firstLineInfo.iterkeys())
        for key in actions:
            if key not in lineInfo:
                errors.append(_(
                    "Line %(num)d: Require \"%(action)s = <status-list> -> "
                    "<new-status>\" line.",
                    num=firstLineInfo[key], action=key))

        if len(errors) != 0:
            out['textError'] = errors;
            return

        contents = '\n'.join(['[ticket-workflow]'] + lines).encode('utf-8')
        tmp_fd, tmp_file = mkstemp('.ini', 'workflow-admin')
        try:
            tmp_fp = os.fdopen(tmp_fd, 'w')
            tmp_fd = None
            try:
                tmp_fp.write(contents)
            finally:
                tmp_fp.close()
            tmp_conf = Configuration(tmp_file)
        finally:
            if tmp_fd is not None:
                os.close(tmp_fd)
            os.remove(tmp_file)

        try:
            out['actions'], out['status'] = self._conf_to_inner_format(tmp_conf)
        except ConfigParser.Error, e:
            out['textError'] = [to_unicode(e)]
            return
        out['lineInfo'] = lineInfo
        out['firstLineInfo'] = firstLineInfo
        out['others'] = others
示例#3
0
class QueuesAjaxModule(Component):
    implements(IRequestHandler)

    audit = ChoiceOption(
        'queues',
        'audit',
        choices=['log', 'ticket', 'none'],
        doc="Record reorderings in log, in ticket, or not at all.")

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/queuesajax')

    def process_request(self, req):
        """Process AJAX request.  Args come back in this form:

          id5=position1&id23=position2

        where the name is the ticket id prefixed with 'id' and the
        value is the new position value prefixed with the name of
        the first column in the report.

        IMPORTANT: DO NOT RENAME THE FIRST COLUMN IN THE REPORT!
        This code assumes that the name of the first column in the
        report exactly matches the ticket's field name.  This is
        to allow any position field name versus hard-coding it.
        """
        try:
            changes = self._get_changes(req.args)
            self._save_changes(changes, req.authname)
            code, msg = 200, "OK"
        except Exception:
            import traceback
            code, msg = 500, "Oops...\n" + traceback.format_exc() + "\n"
        req.send_response(code)
        req.send_header('Content-Type', 'text/plain')
        req.send_header('Content-Length', len(msg))
        req.end_headers()
        req.write(msg)

    # private methods
    def _get_changes(self, args):
        """Extract ticket ids and new position values from request args
        that are returned in this form:

          id5=position1&id23=position2
        """
        changes = {}
        keyval_re = re.compile(r"(?P<key>[^0-9]+)(?P<val>[0-9]*)")
        for key, val in args.items():
            # get ticket id
            match = keyval_re.search(key)
            if not match:
                continue
            id = match.groupdict()['val']
            if not id:
                continue

            # get position field name and value
            match = keyval_re.search(val)
            if not match:
                continue
            field = match.groupdict()['key']
            new_pos = match.groupdict().get('val', '')

            changes[id] = (field, new_pos)
        return changes

    def _save_changes(self, changes, author):
        """Save ticket changes."""
        if self.audit in ('log', 'none'):
            with self.env.db_transaction as db:
                cursor = db.cursor()
                for id, (field, new_pos) in changes.items():
                    cursor.execute(
                        """
                        SELECT value from ticket_custom
                         WHERE name=%s AND ticket=%s
                        """, (field, id))
                    result = cursor.fetchone()
                    if result:
                        old_pos = result[0]
                        cursor.execute(
                            """
                            UPDATE ticket_custom SET value=%s
                             WHERE name=%s AND ticket=%s
                            """, (new_pos, field, id))
                    else:
                        old_pos = '(none)'
                        cursor.execute(
                            """
                            INSERT INTO ticket_custom (ticket,name,value)
                             VALUES (%s,%s,%s)
                            """, (id, field, new_pos))
                    if self.audit == 'log':
                        self.log.info(
                            "%s reordered ticket #%s's %s from %s to %s" %
                            (author, id, field, old_pos, new_pos))
        else:
            for id, (field, new_pos) in changes.items():
                ticket = Ticket(self.env, id)
                ticket[field] = new_pos
                ticket.save_changes(author=author, comment='')
示例#4
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)
示例#5
0
文件: svn_fs.py 项目: t2y/trac
class SubversionConnector(Component):

    implements(ISystemInfoProvider, IRepositoryConnector)

    required = False

    branches = ListOption('svn', 'branches', 'trunk, branches/*', doc=
        """Comma separated list of paths categorized as branches.
        If a path ends with '*', then all the directory entries found below
        that path will be included.
        Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*`
        """)

    tags = ListOption('svn', 'tags', 'tags/*', doc=
        """Comma separated list of paths categorized as tags.

        If a path ends with '*', then all the directory entries found below
        that path will be included.
        Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1`
        """)

    eol_style = ChoiceOption(
        'svn', 'eol_style', ['native', 'LF', 'CRLF', 'CR'], doc=
        """End-of-Line character sequences when `svn:eol-style` property is
        `native`.

        If `native`, substitute with the native EOL marker on the server.
        Otherwise, if `LF`, `CRLF` or `CR`, substitute with the specified
        EOL marker.

        (''since 1.0.2'')""")

    error = None

    def __init__(self):
        self._version = None
        try:
            _import_svn()
            self.log.debug('Subversion bindings imported')
        except ImportError as e:
            self.error = e
            self.log.info('Failed to load Subversion bindings', exc_info=True)
        else:
            version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR,
                       core.SVN_VER_MICRO)
            self._version = '%d.%d.%d' % version + core.SVN_VER_TAG
            if version[0] < 1:
                self.error = _("Subversion >= 1.0 required, found %(version)s",
                               version=self._version)
            Pool()

    # ISystemInfoProvider methods

    def get_system_info(self):
        if self.required:
            yield 'Subversion', self._version

    # IRepositoryConnector methods

    def get_supported_types(self):
        prio = 1
        if self.error:
            prio = -1
        yield ("direct-svnfs", prio * 4)
        yield ("svnfs", prio * 4)
        yield ("svn", prio * 2)

    def get_repository(self, type, dir, params):
        """Return a `SubversionRepository`.

        The repository is wrapped in a `CachedRepository`, unless `type` is
        'direct-svnfs'.
        """
        params.update(tags=self.tags, branches=self.branches)
        params.setdefault('eol_style', self.eol_style)
        repos = SubversionRepository(dir, params, self.log)
        if type != 'direct-svnfs':
            repos = SvnCachedRepository(self.env, repos, self.log)
        self.required = True
        return repos
示例#6
0
class MasterTicketsModule(Component):
    """Provides support for ticket dependencies."""

    implements(IRequestFilter, IRequestHandler, ITemplateProvider,
               ITemplateStreamFilter)

    dot_path = Option('mastertickets',
                      'dot_path',
                      default='dot',
                      doc="Path to the dot executable.")

    gs_path = Option('mastertickets',
                     'gs_path',
                     default='gs',
                     doc="Path to the ghostscript executable.")

    use_gs = BoolOption(
        'mastertickets',
        'use_gs',
        default=False,
        doc="If enabled, use ghostscript to produce nicer output.")

    acceptable_formats = ListOption(
        'mastertickets',
        'acceptable_formats',
        default='png,cmapx',
        sep=',',
        doc="""The formats that may be chosen; execute dot -T? for a
            list of options.""")

    closed_color = Option('mastertickets',
                          'closed_color',
                          default='green',
                          doc="Color of closed tickets")

    opened_color = Option('mastertickets',
                          'opened_color',
                          default='red',
                          doc="Color of opened tickets")

    show_key = Option('mastertickets',
                      'show_key',
                      default=False,
                      doc="Show a key for open/closed nodes")

    closed_text = Option('mastertickets',
                         'closed_text',
                         default='Done',
                         doc="Text for key showing closed tickets")

    opened_text = Option('mastertickets',
                         'opened_text',
                         default='ToDo',
                         doc="Text for key showing opened tickets")

    highlight_target = Option('mastertickets',
                              'highlight_target',
                              default=False,
                              doc="Highlight target tickets in graph")

    full_graph = Option(
        'mastertickets',
        'full_graph',
        default=False,
        doc="Show full dep. graph, not just direct blocking links")

    graph_direction = ChoiceOption(
        'mastertickets',
        'graph_direction',
        choices=['TD', 'LR', 'DT', 'RL'],
        doc="""Direction of the dependency graph (TD = Top Down,
            DT = Down Top, LR = Left Right, RL = Right Left).""")

    fields = set(['blocking', 'blockedby'])

    # IRequestFilter methods

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

    def post_process_request(self, req, template, data, content_type):
        if template is not None:
            if req.path_info.startswith('/ticket/'):
                # In case of an invalid ticket, the data is invalid
                if not data:
                    return template, data, content_type
                tkt = data['ticket']
                links = TicketLinks(self.env, tkt)

                for i in links.blocked_by:
                    if Ticket(self.env, i)['status'] != 'closed':
                        add_script(req, 'mastertickets/js/disable_resolve.js')
                        break

                # Add link to depgraph if needed.
                if links:
                    add_ctxtnav(req, 'Depgraph',
                                req.href.depgraph('ticket', tkt.id))

                for change in data.get('changes', {}):
                    if 'fields' not in change:
                        continue
                    for field, field_data in change['fields'].iteritems():
                        if field in self.fields:
                            # Skip if rendered already
                            if 'rendered' in field_data:
                                continue

                            if field_data['new'].strip():
                                new = to_int_set(field_data['new'])
                            else:
                                new = set()
                            if field_data['old'].strip():
                                old = to_int_set(field_data['old'])
                            else:
                                old = set()
                            add = new - old
                            sub = old - new
                            elms = html()
                            if add:
                                elms.append(
                                    html.em(u', '.join(
                                        unicode(n) for n in sorted(add))))
                                elms.append(u' added')
                            if add and sub:
                                elms.append(u'; ')
                            if sub:
                                elms.append(
                                    html.em(u', '.join(
                                        unicode(n) for n in sorted(sub))))
                                elms.append(u' removed')
                            field_data['rendered'] = elms

            # Add a link to generate a dependency graph for all the tickets
            # in the milestone
            if req.path_info.startswith('/milestone/'):
                if not data:
                    return template, data, content_type
                milestone = data['milestone']
                add_ctxtnav(req, 'Depgraph',
                            req.href.depgraph('milestone', milestone.name))

        return template, data, content_type

    # ITemplateStreamFilter methods

    def filter_stream(self, req, method, filename, stream, data):
        if not data:
            return stream

        # Try all at the same time to catch changed or processed templates.
        if filename in [
                'report_view.html', 'query_results.html', 'ticket.html',
                'query.html'
        ]:
            # For ticket.html
            if 'fields' in data and isinstance(data['fields'], list):
                for field in data['fields']:
                    for f in self.fields:
                        if field['name'] == f and data['ticket'][f]:
                            field['rendered'] = \
                                self._link_tickets(req, data['ticket'][f])
            # For query_results.html and query.html
            if 'groups' in data and isinstance(data['groups'], list):
                for group, tickets in data['groups']:
                    for ticket in tickets:
                        for f in self.fields:
                            if f in ticket:
                                ticket[f] = self._link_tickets(req, ticket[f])
            # For report_view.html
            if 'row_groups' in data and isinstance(data['row_groups'], list):
                for group, rows in data['row_groups']:
                    for row in rows:
                        if 'cell_groups' in row and \
                                isinstance(row['cell_groups'], list):
                            for cells in row['cell_groups']:
                                for cell in cells:
                                    # If the user names column in the report
                                    # differently (blockedby AS "blocked by")
                                    # then this will not find it
                                    if cell.get('header', {}).get('col') \
                                            in self.fields:
                                        cell['value'] = \
                                            self._link_tickets(req,
                                                               cell['value'])
        return stream

    # ITemplateProvider methods

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

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(
            r'^/depgraph/(?P<realm>ticket|milestone)/'
            r'(?P<id>((?!depgraph.png).)+)(/depgraph.png)?$', req.path_info)
        if match:
            req.args['realm'] = match.group('realm')
            req.args['id'] = match.group('id')
            return True

    def process_request(self, req):
        realm = req.args['realm']
        id_ = req.args['id']

        if not which(self.dot_path):
            raise TracError(
                _("Path to dot executable is invalid: %(path)s",
                  path=self.dot_path))

        # Urls to generate the depgraph for a ticket is /depgraph/ticketnum
        # Urls to generate the depgraph for a milestone is
        # /depgraph/milestone/milestone_name

        # List of tickets to generate the depgraph.
        if realm == 'milestone':
            # We need to query the list of tickets in the milestone
            query = Query(self.env, constraints={'milestone': [id_]}, max=0)
            tkt_ids = [fields['id'] for fields in query.execute(req)]
        else:
            tid = as_int(id_, None)
            if tid is None:
                raise TracError(
                    tag_("%(id)s is not a valid ticket id.", id=html.tt(id_)))
            tkt_ids = [tid]

        # The summary argument defines whether we place the ticket id or
        # its summary in the node's label
        label_summary = 0
        if 'summary' in req.args:
            label_summary = int(req.args.get('summary'))

        g = self._build_graph(req, tkt_ids, label_summary=label_summary)
        if req.path_info.endswith('/depgraph.png') or 'format' in req.args:
            format_ = req.args.get('format')
            if format_ == 'text':
                # In case g.__str__ returns unicode, convert it in ascii
                req.send(
                    to_unicode(g).encode('ascii', 'replace'), 'text/plain')
            elif format_ == 'debug':
                import pprint

                req.send(
                    pprint.pformat(
                        [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]),
                    'text/plain')
            elif format_ is not None:
                if format_ in self.acceptable_formats:
                    req.send(g.render(self.dot_path, format_), 'text/plain')
                else:
                    raise TracError(
                        _("The %(format)s format is not allowed.",
                          format=format_))

            if self.use_gs:
                ps = g.render(self.dot_path, 'ps2')
                gs = subprocess.Popen([
                    self.gs_path, '-q', '-dTextAlphaBits=4',
                    '-dGraphicsAlphaBits=4', '-sDEVICE=png16m',
                    '-sOutputFile=%stdout%', '-'
                ],
                                      stdin=subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
                img, err = gs.communicate(ps)
                if err:
                    self.log.debug('MasterTickets: Error from gs: %s', err)
            else:
                img = g.render(self.dot_path)
            req.send(img, 'image/png')
        else:
            data = {}

            # Add a context link to enable/disable labels in nodes.
            if label_summary:
                add_ctxtnav(req, 'Without labels',
                            req.href(req.path_info, summary=0))
            else:
                add_ctxtnav(req, 'With labels',
                            req.href(req.path_info, summary=1))

            if realm == 'milestone':
                add_ctxtnav(req, 'Back to Milestone: %s' % id_,
                            req.href.milestone(id_))
                data['milestone'] = id_
            else:
                data['ticket'] = id_
                add_ctxtnav(req, 'Back to Ticket #%s' % id_,
                            req.href.ticket(id_))
            data['graph'] = g
            data['graph_render'] = functools.partial(g.render, self.dot_path)
            data['use_gs'] = self.use_gs

            return 'depgraph.html', data, None

    def _build_graph(self, req, tkt_ids, label_summary=0):
        g = graphviz.Graph(log=self.log)
        g.label_summary = label_summary

        g.attributes['rankdir'] = self.graph_direction

        node_default = g['node']
        node_default['style'] = 'filled'

        edge_default = g['edge']
        edge_default['style'] = ''

        # Force this to the top of the graph
        for tid in tkt_ids:
            g[tid]

        if self.show_key:
            g[-1]['label'] = self.closed_text
            g[-1]['fillcolor'] = self.closed_color
            g[-1]['shape'] = 'box'
            g[-2]['label'] = self.opened_text
            g[-2]['fillcolor'] = self.opened_color
            g[-2]['shape'] = 'box'

        links = TicketLinks.walk_tickets(self.env, tkt_ids, self.full_graph)
        links = sorted(links, key=lambda link: link.tkt.id)
        for link in links:
            tkt = link.tkt
            node = g[tkt.id]
            if label_summary:
                node['label'] = u'#%s %s' % (tkt.id, tkt['summary'])
            else:
                node['label'] = u'#%s' % tkt.id
            node['fillcolor'] = tkt['status'] == 'closed' and \
                                self.closed_color or self.opened_color
            node['URL'] = req.href.ticket(tkt.id)
            node['alt'] = u'Ticket #%s' % tkt.id
            node['tooltip'] = escape(tkt['summary'])
            if self.highlight_target and tkt.id in tkt_ids:
                node['penwidth'] = 3

            for n in link.blocking:
                node > g[n]

        return g

    def _link_tickets(self, req, tickets):
        items = []

        for i, word in enumerate(re.split(r'([;,\s]+)', tickets)):
            if i % 2:
                items.append(word)
            elif word:
                tid = word
                word = '#%s' % word

                try:
                    ticket = Ticket(self.env, tid)
                    if 'TICKET_VIEW' in req.perm(ticket.resource):
                        word = \
                            html.a(
                                '#%s' % ticket.id,
                                href=req.href.ticket(ticket.id),
                                class_=classes(ticket['status'], 'ticket'),
                                title=get_resource_summary(self.env,
                                                           ticket.resource)
                            )
                except ResourceNotFound:
                    pass

                items.append(word)

        if items:
            return html(items)
        else:
            return None
class SubTicketsModule(Component):

    implements(IRequestFilter, ITicketManipulator, ITemplateProvider,
               ITemplateStreamFilter)

    # Simple Options

    opt_skip_validation = ListOption('subtickets',
                                     'skip_closure_validation',
                                     default=[],
                                     doc=_("""
         Normally, reopening a child with a `closed` parent will be
         refused and closing a parent with non-`closed` children will also
         be refused. Adding either of `reopen` or `resolve` to this option will
         make Subtickets skip this validation for the respective action.
         Separate by comma if both actions are listed.

         Caveat: This functionality will be made workflow-independent in a
         future release of !SubTicketsPlugin.
         """))

    opt_recursion_depth = IntOption('subtickets',
                                    'recursion_depth',
                                    default=-1,
                                    doc=_("""
         Limit the number of recursive levels when listing subtickets.
         Default is infinity, represented by`-1`. The value zero (0)
         limits the listing to immediate children.
         """))

    opt_add_style = ChoiceOption('subtickets',
                                 'add_style', ['button', 'link'],
                                 doc=_("""
         Choose whether to make `Add` look like a button (default) or a link
         """))

    opt_owner_url = Option('subtickets',
                           'owner_url',
                           doc=_("""
                           Currently undocumented.
                           """))

    # Per-ticket type options -- all initialised in __init__()

    opt_inherit_fields = dict()
    opt_columns = dict()

    def _add_per_ticket_type_option(self, ticket_type):

        self.opt_inherit_fields[ticket_type] = ListOption(
            'subtickets',
            'type.%s.child_inherits' % ticket_type,
            default='',
            doc=_("""Comma-separated list of ticket fields whose values are
            to be copied from a parent ticket into a newly created
            child ticket.
            """))

        self.opt_columns[ticket_type] = ListOption('subtickets',
                                                   'type.%s.table_columns' %
                                                   ticket_type,
                                                   default='status,owner',
                                                   doc=_("""
             Comma-separated list of ticket fields whose values are to be
             shown for each child ticket in the subtickets list
             """))

    def __init__(self):
        # The following initialisations must happen inside init()
        # in order to be able to access self.env
        for tt in TicketType.select(self.env):
            self._add_per_ticket_type_option(tt.name)

    # ITemplateProvider methods

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

    def get_templates_dirs(self):
        return []

    # 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'):
            # get parent ticket's data
            if data and 'ticket' in data:
                ticket = data['ticket']
                parents = ticket['parents'] or ''
                ids = set(NUMBERS_RE.findall(parents))

                if len(parents) > 0:
                    self._append_parent_links(req, data, ids)

                children = self.get_children(ticket.id)
                if children:
                    data['subtickets'] = children

        elif path.startswith('/admin/ticket/type') \
                and data \
                and set(['add', 'name']).issubset(data.keys()) \
                and data['add'] == 'Add':
            self._add_per_ticket_type_option(data['name'])

        return template, data, content_type

    def _append_parent_links(self, req, data, ids):
        links = []
        for id in sorted(ids, key=lambda x: int(x)):
            try:
                ticket = Ticket(self.env, id)
                elem = tag.a('#%s' % id,
                             href=req.href.ticket(id),
                             class_='%s ticket' % ticket['status'],
                             title=ticket['summary'])
                if len(links) > 0:
                    links.append(', ')
                links.append(elem)
            except ResourceNotFound:
                pass
        for field in data.get('fields', ''):
            if field.get('name') == 'parents':
                field['rendered'] = tag.span(*links)

    # ITicketManipulator methods

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

    def get_children(self, parent_id, depth=0):
        children = {}

        for parent, child in self.env.db_query(
                """
                SELECT parent, child FROM subtickets WHERE parent=%s
                """, (parent_id, )):
            children[child] = None

        if self.opt_recursion_depth > depth or self.opt_recursion_depth == -1:
            for id in children:
                children[id] = self.get_children(id, depth + 1)

        return children

    def validate_ticket(self, req, ticket):
        action = req.args.get('action')

        if action in self.opt_skip_validation:
            return

        if action == 'resolve':

            for parent, child in self.env.db_query(
                    """
                    SELECT parent, child FROM subtickets WHERE parent=%s
                    """, (ticket.id, )):
                if Ticket(self.env, child)['status'] != 'closed':
                    yield None, _("""Cannot close/resolve because child
                         ticket #%(child)s is still open""",
                                  child=child)

        elif action == 'reopen':
            ids = set(NUMBERS_RE.findall(ticket['parents'] or ''))
            for id in ids:
                if Ticket(self.env, id)['status'] == 'closed':
                    msg = _(
                        "Cannot reopen because parent ticket #%(id)s "
                        "is closed",
                        id=id)
                    yield None, msg

    # ITemplateStreamFilter method

    def _create_subtickets_table(self, req, children, tbody, depth=0):
        """Recursively create list table of subtickets
        """
        if not children:
            return
        for id in sorted(children, key=lambda x: int(x)):
            ticket = Ticket(self.env, id)

            # the row
            r = []
            # Always show ID and summary
            attrs = {'href': req.href.ticket(id)}
            if ticket['status'] == 'closed':
                attrs['class_'] = 'closed'
            link = tag.a('#%s' % id, **attrs)
            summary = tag.td(link,
                             ': %s' % ticket['summary'],
                             style='padding-left: %dpx;' % (depth * 15))
            r.append(summary)

            # Add other columns as configured.
            for column in \
                    self.env.config.getlist('subtickets',
                                            'type.%(type)s.table_columns'
                                            % ticket):
                if column == 'owner':
                    if self.opt_owner_url:
                        href = req.href(self.opt_owner_url % ticket['owner'])
                    else:
                        href = req.href.query(status='!closed',
                                              owner=ticket['owner'])
                    e = tag.td(tag.a(ticket['owner'], href=href))
                elif column == 'milestone':
                    href = req.href.query(status='!closed',
                                          milestone=ticket['milestone'])
                    e = tag.td(tag.a(ticket['milestone'], href=href))
                else:
                    e = tag.td(ticket[column])
                r.append(e)
            tbody.append(tag.tr(*r))

            self._create_subtickets_table(req, children[id], tbody, depth + 1)

    def filter_stream(self, req, method, filename, stream, data):
        if not req.path_info.startswith('/ticket/'):
            return stream

        div = None
        link = None
        button = None

        if 'ticket' in data:
            # get parents data
            ticket = data['ticket']
            # title
            div = tag.div(class_='description')
            if 'TICKET_CREATE' in req.perm(ticket.resource) \
                    and ticket['status'] != 'closed':
                opt_inherit = self.env.config.getlist(
                    'subtickets', 'type.%(type)s.child_inherits' % ticket)
                if self.opt_add_style == 'link':
                    inh = {f: ticket[f] for f in opt_inherit}
                    link = tag.a(_('add'),
                                 href=req.href.newticket(parents=ticket.id,
                                                         **inh))
                    link = tag.span('(', link, ')', class_='addsubticket')
                else:
                    inh = [
                        tag.input(type='hidden', name=f, value=ticket[f])
                        for f in opt_inherit
                    ]

                    button = tag.form(tag.div(tag.input(
                        type="submit",
                        value=_("Create"),
                        title=_("Create a child ticket")),
                                              inh,
                                              tag.input(type="hidden",
                                                        name="parents",
                                                        value=str(ticket.id)),
                                              class_="inlinebuttons"),
                                      method="get",
                                      action=req.href.newticket())
            div.append(button)
            div.append(tag.h3(_('Subtickets '), link))

        if 'subtickets' in data:
            # table
            tbody = tag.tbody()
            div.append(tag.table(tbody, class_='subtickets'))
            # tickets
            self._create_subtickets_table(req, data['subtickets'], tbody)

        if div:
            add_stylesheet(req, 'subtickets/css/subtickets.css')
            '''
            If rendered in preview mode, DIV we're interested in isn't a child
            but the root and transformation won't succeed.
            According to HTML specification, id's must be unique within a
            document, so it's safe to omit the leading '.' in XPath expression
            to select all matching regardless of hierarchy their in.
            '''
            stream |= Transformer('//div[@id="ticket"]').append(div)

        return stream
示例#8
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
示例#9
0
class MasterTicketsModule(Component):
    """Provides support for ticket dependencies."""

    implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter,
               ITemplateProvider, ITicketManipulator)

    dot_path = Option('mastertickets',
                      'dot_path',
                      default='dot',
                      doc='Path to the dot executable.')
    gs_path = Option('mastertickets',
                     'gs_path',
                     default='gs',
                     doc='Path to the ghostscript executable.')
    use_gs = BoolOption(
        'mastertickets',
        'use_gs',
        default=False,
        doc='If enabled, use ghostscript to produce nicer output.')

    closed_color = Option('mastertickets',
                          'closed_color',
                          default='green',
                          doc='Color of closed tickets')
    opened_color = Option('mastertickets',
                          'opened_color',
                          default='red',
                          doc='Color of opened tickets')

    graph_direction = ChoiceOption(
        'mastertickets',
        'graph_direction',
        choices=['TD', 'LR', 'DT', 'RL'],
        doc=
        'Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)'
    )

    FIELD_XPATH = '//div[@id="ticket"]/table[@class="properties"]//td[@headers="h_%s"]/text()'
    fields = set(['blocking', 'blockedby'])

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.path_info.startswith('/ticket/'):
            # In case of an invalid ticket, the data is invalid
            if not data:
                return template, data, content_type
            tkt = data['ticket']
            links = TicketLinks(self.env, tkt)

            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    add_script(req, 'mastertickets/disable_resolve.js')
                    break

            data['mastertickets'] = {
                'field_values': {
                    'blocking': linkify_ids(self.env, req, links.blocking),
                    'blockedby': linkify_ids(self.env, req, links.blocked_by),
                },
            }

            # Add link to depgraph if needed
            if links:
                add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id))

            for change in data.get('changes', {}):
                if not change.has_key('fields'):
                    continue
                for field, field_data in change['fields'].iteritems():
                    if field in self.fields:
                        if field_data['new'].strip():
                            new = set(
                                [int(n) for n in field_data['new'].split(',')])
                        else:
                            new = set()
                        if field_data['old'].strip():
                            old = set(
                                [int(n) for n in field_data['old'].split(',')])
                        else:
                            old = set()
                        add = new - old
                        sub = old - new
                        elms = tag()
                        if add:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(add)])))
                            elms.append(u' added')
                        if add and sub:
                            elms.append(u'; ')
                        if sub:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(sub)])))
                            elms.append(u' removed')
                        field_data['rendered'] = elms

        #add a link to generate a dependency graph for all the tickets in the milestone
        if req.path_info.startswith('/milestone/'):
            if not data:
                return template, data, content_type
            milestone = data['milestone']
            add_ctxtnav(req, 'Depgraph',
                        req.href.depgraph('milestone', milestone.name))

        return template, data, content_type

    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        if 'mastertickets' in data:
            for field, value in data['mastertickets'][
                    'field_values'].iteritems():
                stream |= Transformer(self.FIELD_XPATH % field).replace(value)
        return stream

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

    def validate_ticket(self, req, ticket):
        if req.args.get('action') == 'resolve' and req.args.get(
                'action_resolve_resolve_resolution') == 'fixed':
            links = TicketLinks(self.env, ticket)
            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    yield None, 'Ticket #%s is blocking this ticket' % i

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return [('mastertickets', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        return [resource_filename(__name__, 'templates')]

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/depgraph')

    def process_request(self, req):
        path_info = req.path_info[10:]

        if not path_info:
            raise TracError('No ticket specified')

        #list of tickets to generate the depgraph for
        tkt_ids = []
        milestone = None
        split_path = path_info.split('/', 2)

        #Urls to generate the depgraph for a ticket is /depgraph/ticketnum
        #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name
        if split_path[0] == 'milestone':
            #we need to query the list of tickets in the milestone
            milestone = split_path[1]
            query = Query(self.env,
                          constraints={'milestone': [milestone]},
                          max=0)
            tkt_ids = [fields['id'] for fields in query.execute()]
        else:
            #the list is a single ticket
            tkt_ids = [int(split_path[0])]

        #the summary argument defines whether we place the ticket id or
        #it's summary in the node's label
        label_summary = 0
        if 'summary' in req.args:
            label_summary = int(req.args.get('summary'))

        g = self._build_graph(req, tkt_ids, label_summary=label_summary)
        if path_info.endswith('/depgraph.png') or 'format' in req.args:
            format = req.args.get('format')
            if format == 'text':
                #in case g.__str__ returns unicode, we need to convert it in ascii
                req.send(
                    to_unicode(g).encode('ascii', 'replace'), 'text/plain')
            elif format == 'debug':
                import pprint
                req.send(
                    pprint.pformat(
                        [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]),
                    'text/plain')
            elif format is not None:
                req.send(g.render(self.dot_path, format), 'text/plain')

            if self.use_gs:
                ps = g.render(self.dot_path, 'ps2')
                gs = subprocess.Popen([
                    self.gs_path, '-q', '-dTextAlphaBits=4',
                    '-dGraphicsAlphaBits=4', '-sDEVICE=png16m',
                    '-sOutputFile=%stdout%', '-'
                ],
                                      stdin=subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
                img, err = gs.communicate(ps)
                if err:
                    self.log.debug('MasterTickets: Error from gs: %s', err)
            else:
                img = g.render(self.dot_path)
            req.send(img, 'image/png')
        else:
            data = {}

            #add a context link to enable/disable labels in nodes
            if label_summary:
                add_ctxtnav(req, 'Without labels',
                            req.href(req.path_info, summary=0))
            else:
                add_ctxtnav(req, 'With labels',
                            req.href(req.path_info, summary=1))

            if milestone is None:
                tkt = Ticket(self.env, tkt_ids[0])
                data['tkt'] = tkt
                add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id,
                            req.href.ticket(tkt.id))
            else:
                add_ctxtnav(req, 'Back to Milestone %s' % milestone,
                            req.href.milestone(milestone))
            data['milestone'] = milestone
            data['graph'] = g
            data['graph_render'] = partial(g.render, self.dot_path)
            data['use_gs'] = self.use_gs

            return 'depgraph.html', data, None

    def _build_graph(self, req, tkt_ids, label_summary=0):
        g = graphviz.Graph()
        g.label_summary = label_summary

        g.attributes['rankdir'] = self.graph_direction

        node_default = g['node']
        node_default['style'] = 'filled'

        edge_default = g['edge']
        edge_default['style'] = ''

        # Force this to the top of the graph
        for id in tkt_ids:
            g[id]

        links = TicketLinks.walk_tickets(self.env, tkt_ids)
        links = sorted(links, key=lambda link: link.tkt.id)
        for link in links:
            tkt = link.tkt
            node = g[tkt.id]
            if label_summary:
                node['label'] = u'#%s %s' % (tkt.id, tkt['summary'])
            else:
                node['label'] = u'#%s' % tkt.id
            node['fillcolor'] = tkt[
                'status'] == 'closed' and self.closed_color or self.opened_color
            node['URL'] = req.href.ticket(tkt.id)
            node['alt'] = u'Ticket #%s' % tkt.id
            node['tooltip'] = tkt['summary']

            for n in link.blocking:
                node > g[n]

        return g
示例#10
0
class EmailDistributor(Component):

    implements(IAnnouncementDistributor, IAnnouncementPreferenceProvider)

    formatters = ExtensionPoint(IAnnouncementFormatter)
    producers = ExtensionPoint(IAnnouncementProducer)
    distributors = ExtensionPoint(IAnnouncementDistributor)
    # Make ordered
    decorators = ExtensionPoint(IAnnouncementEmailDecorator)

    resolvers = OrderedExtensionsOption('announcer', 'email_address_resolvers',
        IAnnouncementAddressResolver, 'SpecifiedEmailResolver, '\
        'SessionEmailResolver, DefaultDomainEmailResolver',
        """Comma seperated list of email resolver components in the order
        they will be called.  If an email address is resolved, the remaining
        resolvers will not be called.
        """)

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

        This component is used by the announcer system to send emails.
        Currently, `SmtpEmailSender` and `SendmailEmailSender` are provided.
        """)

    enabled = BoolOption('announcer', 'email_enabled', 'true',
                         """Enable email notification.""")

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

    from_name = Option('announcer', 'email_from_name', '',
                       """Sender name to use in notification emails.""")

    replyto = Option('announcer', 'email_replyto', 'trac@localhost',
                     """Reply-To address to use in notification emails.""")

    mime_encoding = ChoiceOption(
        'announcer', 'mime_encoding', ['base64', 'qp', 'none'],
        """Specifies the MIME encoding scheme for emails.

        Valid options are 'base64' for Base64 encoding, 'qp' for
        Quoted-Printable, and 'none' for no encoding. Note that the no encoding
        means that non-ASCII characters in text are going to cause problems
        with notifications.
        """)

    use_public_cc = BoolOption(
        'announcer', 'use_public_cc', 'false',
        """Recipients can see email addresses of other CC'ed recipients.

        If this option is disabled (the default), recipients are put on BCC
        """)

    # used in email decorators, but not here
    subject_prefix = Option(
        'announcer', 'email_subject_prefix', '__default__',
        """Text to prepend to subject line of notification emails.

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

    to = Option('announcer', 'email_to', 'undisclosed-recipients: ;',
                'Default To: field')

    use_threaded_delivery = BoolOption(
        'announcer', 'use_threaded_delivery', 'false',
        """Do message delivery in a separate thread.

        Enabling this will improve responsiveness for requests that end up
        with an announcement being sent over email. It requires building
        Python with threading support enabled-- which is usually the case.
        To test, start Python and type 'import threading' to see
        if it raises an error.
        """)

    default_email_format = Option(
        'announcer', 'default_email_format', 'text/plain',
        """The default mime type of the email notifications.

        This can be overridden on a per user basis through the announcer
        preferences panel.
        """)

    set_message_id = BoolOption(
        'announcer', 'set_message_id', 'true',
        """Disable if you would prefer to let the email server handle
        message-id generation.
        """)

    rcpt_allow_regexp = Option(
        'announcer', 'rcpt_allow_regexp', '',
        """A whitelist pattern to match any address to before adding to
        recipients list.
        """)

    rcpt_local_regexp = Option(
        'announcer', 'rcpt_local_regexp', '',
        """A whitelist pattern to match any address, that should be
        considered local.

        This will be evaluated only if msg encryption is set too.
        Recipients with matching email addresses will continue to
        receive unencrypted email messages.
        """)

    crypto = Option(
        'announcer', 'email_crypto', '',
        """Enable cryptographically operation on email msg body.

        Empty string, the default for unset, disables all crypto operations.
        Valid values are:
            sign          sign msg body with given privkey
            encrypt       encrypt msg body with pubkeys of all recipients
            sign,encrypt  sign, than encrypt msg body
        """)

    # get GnuPG configuration options
    gpg_binary = Option(
        'announcer', 'gpg_binary', 'gpg',
        """GnuPG binary name, allows for full path too.

        Value 'gpg' is same default as in python-gnupg itself.
        For usual installations location of the gpg binary is auto-detected.
        """)

    gpg_home = Option(
        'announcer', 'gpg_home', '', """Directory containing keyring files.

        In case of wrong configuration missing keyring files without content
        will be created in the configured location, provided necessary
        write permssion is granted for the corresponding parent directory.
        """)

    private_key = Option(
        'announcer', 'gpg_signing_key', None,
        """Keyid of private key (last 8 chars or more) used for signing.

        If unset, a private key will be selected from keyring automagicly.
        The password must be available i.e. provided by running gpg-agent
        or empty (bad security). On failing to unlock the private key,
        msg body will get emptied.
        """)

    def __init__(self):
        self.delivery_queue = None
        self._init_pref_encoding()

    def get_delivery_queue(self):
        if not self.delivery_queue:
            self.delivery_queue = Queue.Queue()
            thread = DeliveryThread(self.delivery_queue, self.send)
            thread.start()
        return self.delivery_queue

    # IAnnouncementDistributor
    def transports(self):
        yield "email"

    def formats(self, transport, realm):
        "Find valid formats for transport and realm"
        formats = {}
        for f in self.formatters:
            for style in f.styles(transport, realm):
                formats[style] = f
        self.log.debug(
            "EmailDistributor has found the following formats capable "
            "of handling '%s' of '%s': %s" %
            (transport, realm, ', '.join(formats.keys())))
        if not formats:
            self.log.error("EmailDistributor is unable to continue " \
                    "without supporting formatters.")
        return formats

    def distribute(self, transport, recipients, event):
        found = False
        for supported_transport in self.transports():
            if supported_transport == transport:
                found = True
        if not self.enabled or not found:
            self.log.debug("EmailDistributer email_enabled set to false")
            return
        fmtdict = self.formats(transport, event.realm)
        if not fmtdict:
            self.log.error("EmailDistributer No formats found for %s %s" %
                           (transport, event.realm))
            return
        msgdict = {}
        msgdict_encrypt = {}
        msg_pubkey_ids = []
        # compile pattern before use for better performance
        RCPT_ALLOW_RE = re.compile(self.rcpt_allow_regexp)
        RCPT_LOCAL_RE = re.compile(self.rcpt_local_regexp)

        if self.crypto != '':
            self.log.debug("EmailDistributor attempts crypto operation.")
            self.enigma = CryptoTxt(self.gpg_binary, self.gpg_home)

        for name, authed, addr in recipients:
            fmt = name and \
                self._get_preferred_format(event.realm, name, authed) or \
                self._get_default_format()
            if fmt not in fmtdict:
                self.log.debug(("EmailDistributer format %s not available " +
                                "for %s %s, looking for an alternative") %
                               (fmt, transport, event.realm))
                # If the fmt is not available for this realm, then try to find
                # an alternative
                oldfmt = fmt
                fmt = None
                for f in fmtdict.values():
                    fmt = f.alternative_style_for(transport, event.realm,
                                                  oldfmt)
                    if fmt: break
            if not fmt:
                self.log.error(
                    "EmailDistributer was unable to find a formatter " +
                    "for format %s" % k)
                continue
            rslvr = None
            if name and not addr:
                # figure out what the addr should be if it's not defined
                for rslvr in self.resolvers:
                    addr = rslvr.get_address_for_name(name, authed)
                    if addr: break
            if addr:
                self.log.debug("EmailDistributor found the " \
                        "address '%s' for '%s (%s)' via: %s"%(
                        addr, name, authed and \
                        'authenticated' or 'not authenticated',
                        rslvr.__class__.__name__))

                # ok, we found an addr, add the message
                # but wait, check for allowed rcpt first, if set
                if RCPT_ALLOW_RE.search(addr) is not None:
                    # check for local recipients now
                    local_match = RCPT_LOCAL_RE.search(addr)
                    if self.crypto in ['encrypt', 'sign,encrypt'] and \
                            local_match is None:
                        # search available public keys for matching UID
                        pubkey_ids = self.enigma.get_pubkey_ids(addr)
                        if len(pubkey_ids) > 0:
                            msgdict_encrypt.setdefault(fmt, set()).add(
                                (name, authed, addr))
                            msg_pubkey_ids[len(msg_pubkey_ids):] = pubkey_ids
                            self.log.debug("EmailDistributor got pubkeys " \
                                "for %s: %s" % (addr, pubkey_ids))
                        else:
                            self.log.debug("EmailDistributor dropped %s " \
                                "after missing pubkey with corresponding " \
                                "address %s in any UID" % (name, addr))
                    else:
                        msgdict.setdefault(fmt, set()).add(
                            (name, authed, addr))
                        if local_match is not None:
                            self.log.debug("EmailDistributor expected " \
                                "local delivery for %s to: %s" % (name, addr))
                else:
                    self.log.debug("EmailDistributor dropped %s for " \
                        "not matching allowed recipient pattern %s" % \
                        (addr, self.rcpt_allow_regexp))
            else:
                self.log.debug("EmailDistributor was unable to find an " \
                        "address for: %s (%s)"%(name, authed and \
                        'authenticated' or 'not authenticated'))
        for k, v in msgdict.items():
            if not v or not fmtdict.get(k):
                continue
            self.log.debug("EmailDistributor is sending event as '%s' to: %s" %
                           (fmt, ', '.join(x[2] for x in v)))
            self._do_send(transport, event, k, v, fmtdict[k])
        for k, v in msgdict_encrypt.items():
            if not v or not fmtdict.get(k):
                continue
            self.log.debug(
                "EmailDistributor is sending encrypted info on event " \
                "as '%s' to: %s"%(fmt, ', '.join(x[2] for x in v)))
            self._do_send(transport, event, k, v, fmtdict[k], msg_pubkey_ids)

    def _get_default_format(self):
        return self.default_email_format

    def _get_preferred_format(self, realm, sid, authenticated):
        if authenticated is None:
            authenticated = 0
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            """
            SELECT value
              FROM session_attribute
             WHERE sid=%s
               AND authenticated=%s
               AND name=%s
        """, (sid, int(authenticated), 'announcer_email_format_%s' % realm))
        result = cursor.fetchone()
        if result:
            chosen = result[0]
            self.log.debug("EmailDistributor determined the preferred format" \
                    " for '%s (%s)' is: %s"%(sid, authenticated and \
                    'authenticated' or 'not authenticated', chosen))
            return chosen
        else:
            return self._get_default_format()

    def _init_pref_encoding(self):
        self._charset = Charset()
        self._charset.input_charset = 'utf-8'
        pref = self.mime_encoding.lower()
        if pref == 'base64':
            self._charset.header_encoding = BASE64
            self._charset.body_encoding = BASE64
            self._charset.output_charset = 'utf-8'
            self._charset.input_codec = 'utf-8'
            self._charset.output_codec = 'utf-8'
        elif pref in ['qp', 'quoted-printable']:
            self._charset.header_encoding = QP
            self._charset.body_encoding = QP
            self._charset.output_charset = 'utf-8'
            self._charset.input_codec = 'utf-8'
            self._charset.output_codec = 'utf-8'
        elif pref == 'none':
            self._charset.header_encoding = None
            self._charset.body_encoding = None
            self._charset.input_codec = None
            self._charset.output_charset = 'ascii'
        else:
            raise TracError(_('Invalid email encoding setting: %s' % pref))

    def _message_id(self, realm):
        """Generate an unique message ID."""
        modtime = time.time()
        s = '%s.%d.%s' % (self.env.project_url, modtime,
                          realm.encode('ascii', 'ignore'))
        dig = md5(s).hexdigest()
        host = self.email_from[self.email_from.find('@') + 1:]
        msgid = '<%03d.%s@%s>' % (len(s), dig, host)
        return msgid

    def _filter_recipients(self, rcpt):
        return rcpt

    def _do_send(self,
                 transport,
                 event,
                 format,
                 recipients,
                 formatter,
                 pubkey_ids=[]):

        output = formatter.format(transport, event.realm, format, event)

        # DEVEL: force message body plaintext style for crypto operations
        if self.crypto != '' and pubkey_ids != []:
            if self.crypto == 'sign':
                output = self.enigma.sign(output, self.private_key)
            elif self.crypto == 'encrypt':
                output = self.enigma.encrypt(output, pubkey_ids)
            elif self.crypto == 'sign,encrypt':
                output = self.enigma.sign_encrypt(output, pubkey_ids,
                                                  self.private_key)

            self.log.debug(output)
            self.log.debug(_("EmailDistributor crypto operaton successful."))
            alternate_output = None
        else:
            alternate_style = formatter.alternative_style_for(
                transport, event.realm, format)
            if alternate_style:
                alternate_output = formatter.format(transport, event.realm,
                                                    alternate_style, event)
            else:
                alternate_output = None

        # sanity check
        if not self._charset.body_encoding:
            try:
                dummy = output.encode('ascii')
            except UnicodeDecodeError:
                raise TracError(_("Ticket contains non-ASCII chars. " \
                                  "Please change encoding setting"))

        rootMessage = MIMEMultipart("related")

        headers = dict()
        if self.set_message_id:
            # A different, predictable, but still sufficiently unique
            # message ID will be generated as replacement in
            # announcer.email_decorators.generic.ThreadingEmailDecorator
            # for email threads to work.
            headers['Message-ID'] = self._message_id(event.realm)
        headers['Date'] = formatdate()
        from_header = formataddr((self.from_name
                                  or self.env.project_name, self.email_from))
        headers['From'] = from_header
        headers['To'] = '"%s"' % (self.to)
        if self.use_public_cc:
            headers['Cc'] = ', '.join([x[2] for x in recipients if x])
        headers['Reply-To'] = self.replyto
        for k, v in headers.iteritems():
            set_header(rootMessage, k, v)

        rootMessage.preamble = 'This is a multi-part message in MIME format.'
        if alternate_output:
            parentMessage = MIMEMultipart('alternative')
            rootMessage.attach(parentMessage)

            alt_msg_format = 'html' in alternate_style and 'html' or 'plain'
            msgText = MIMEText(alternate_output, alt_msg_format)
            parentMessage.attach(msgText)
        else:
            parentMessage = rootMessage

        msg_format = 'html' in format and 'html' or 'plain'
        msgText = MIMEText(output, msg_format)
        del msgText['Content-Transfer-Encoding']
        msgText.set_charset(self._charset)
        parentMessage.attach(msgText)
        decorators = self._get_decorators()
        if len(decorators) > 0:
            decorator = decorators.pop()
            decorator.decorate_message(event, rootMessage, decorators)

        recip_adds = [x[2] for x in recipients if x]
        # Append any to, cc or bccs added to the recipient list
        for field in ('To', 'Cc', 'Bcc'):
            if rootMessage[field] and \
                    len(str(rootMessage[field]).split(',')) > 0:
                for addy in str(rootMessage[field]).split(','):
                    self._add_recipient(recip_adds, addy)
        # replace with localized bcc hint
        if headers['To'] == 'undisclosed-recipients: ;':
            set_header(rootMessage, 'To', _('undisclosed-recipients: ;'))

        self.log.debug("Content of recip_adds: %s" % (recip_adds))
        package = (from_header, recip_adds, rootMessage.as_string())
        start = time.time()
        if self.use_threaded_delivery:
            self.get_delivery_queue().put(package)
        else:
            self.send(*package)
        stop = time.time()
        self.log.debug("EmailDistributor took %s seconds to send."\
                %(round(stop-start,2)))

    def send(self, from_addr, recipients, message):
        """Send message to recipients via e-mail."""
        # Ensure the message complies with RFC2822: use CRLF line endings
        message = CRLF.join(re.split("\r?\n", message))
        self.email_sender.send(from_addr, recipients, message)

    def _get_decorators(self):
        return self.decorators[:]

    def _add_recipient(self, recipients, addy):
        if addy.strip() != '"undisclosed-recipients: ;"':
            recipients.append(addy)

    # IAnnouncementDistributor
    def get_announcement_preference_boxes(self, req):
        yield "email", _("E-Mail Format")

    def render_announcement_preference_box(self, req, panel):
        supported_realms = {}
        for producer in self.producers:
            for realm in producer.realms():
                for distributor in self.distributors:
                    for transport in distributor.transports():
                        for fmtr in self.formatters:
                            for style in fmtr.styles(transport, realm):
                                if realm not in supported_realms:
                                    supported_realms[realm] = set()
                                supported_realms[realm].add(style)

        if req.method == "POST":
            for realm in supported_realms:
                opt = req.args.get('email_format_%s' % realm, False)
                if opt:
                    req.session['announcer_email_format_%s' % realm] = opt
        prefs = {}
        for realm in supported_realms:
            prefs[realm] = req.session.get('announcer_email_format_%s' % realm,
                                           None) or self._get_default_format()
        data = dict(
            realms=supported_realms,
            preferences=prefs,
        )
        return "prefs_announcer_email.html", data
示例#11
0
class Navigation(Component):
    """This plugin enables user to choose, if she uses a different (means more
elegant way) display of naviagtion bar.

Type of displaying menu. Possible types are:
`normal` (default): No integration - ''leaves menu as in Trac standard'' 
`fixed_menu`: fixes menu incl. context navigation on top 
of browser window (under DEVELOPMENT!!),
`buttom_ctx_menu`: adds context menu at buttom of page, if available
"""
    implements(IRequestFilter, ITemplateProvider, ITemplateStreamFilter)

    display_navigation = ChoiceOption('trac', SESSION_KEYS['nav'],
        choices=DISPLAY_CHOICES,
        doc="""Type of displaying menu. Possible types are:
`normal` (default): No integration - ''leaves menu as in Trac standard'' 
`fixed_menu`: fixes menu incl. context navigation on top 
of browser window (under DEVELOPMENT!!),
`buttom_ctx_menu`: adds context menu at buttom of page, if available""")
    wiki_link = Option('mainnav', SESSION_KEYS['wiki'], default=None, doc='',
                 doc_domain='tracini')
    ticket_link = Option('mainnav', SESSION_KEYS['tickets'], default=None, doc='',
                 doc_domain='tracini')
    
    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        display = self.get_display(req)
        
        if not display == 'normal':
            # only do something if specified
            if 'fixed_menu' == display:
                add_stylesheet(req, 'navpl/fixed_menu.css')
            
        return (template, data, content_type)
        
    # ITemplateProvider methods
    def get_templates_dirs(self):
        return
    
    def get_htdocs_dirs(self):
        return [('navpl', resource_filename(__name__, 'htdocs'))]  
    
    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        display = self.get_display(req)
        self.__inject_links(req, data['chrome'])
 
        if not display == 'normal':
            # non-standard"
            if 'fixed_menu' == display:
                return self._add_style_attributes(req, stream)
            elif 'buttom_ctx_menu' == display:
                return self._add_buttom_ctx_menu(req, stream)
        
        return stream

    def __inject_links(self, req, chrome_data):
        if chrome_data and chrome_data.has_key('nav'):
            nav = chrome_data['nav']
            if nav and nav.has_key('mainnav'):
                repl_links = self.get_repl_links(req)
                if len(repl_links) > 0: 
                    mainnav = nav['mainnav']
                    for nav in mainnav:
#                        self.log.info('nav: %s' % nav)
                        if nav.has_key('name') \
                        and nav['name'] in repl_links.keys():
                            self.replace_links(req, nav, 
                                               repl_links[nav['name']])
                            
    def replace_links(self, req, nav, new_link):
        wiki_label = nav['label']
        name = '---'
        try:
            name = wiki_label.children
            if nav['name'] == 'wiki':
                nav['label'] = tag.a(name,
                                     href=req.href.wiki(new_link))
            elif nav['name'] == 'tickets':
                if new_link == 'report':
                    nav['label'] = tag.a(name, href=req.href.report())
                elif new_link == 'query':
                    nav['label'] = tag.a(name, href=req.href.query())
                else:
                    nav['label'] = tag.a(name, href=req.href.report(new_link))
        except Exception, e:
            self.log.error(e)
示例#12
0
class QueueDependencyAnalysis(Component, Analysis):
    """Building on mastertickets' blockedby relationships and the queue's
    position, this analyzes a report's tickets for two issues:
    
     1. Detects when dependent tickets are in the wrong queue.
     2. Detects when dependent tickets' positions are out of order.
    
    Specify which reports can be analyzed with the queue_reports option:
    
     [analyze]
     queue_reports = 1,2,3,9
    
    In the example above, this analysis is available for reports 1, 2, 3
    and 9.  If no queue_reports is provided, then the queue's full list of
    reports will be used instead from the [queues] 'reports' option.
    
    The queue_fields config option is the list of fields that define
    a queue.  You can optionally override with a report-specific option:
    
     [analyze]
     queue_fields = milestone,queue
     queue_fields.2 = queue
     queue_fields.9 = queue,phase!=verifying|readying
    
    In the example above, reports 1 and 3 are defined by fields 'milestone'
    and 'queue', report 2 is defined only by field 'queue', and report 9
    is defined by field 'queue' as well as filtering the 'phase' field.
    
    The filtering spec should usually match those in the report - i.e., via
    a pipe-delimited list specify which tickets to include ('=') or not
    include ('!=') in the analysis."""

    implements(IAnalysis)

    reports1 = ListOption('analyze',
                          'queue_reports',
                          default=[],
                          doc="Reports that can be queue dependency analyzed.")
    reports2 = ListOption('queues',
                          'reports',
                          default=[],
                          doc="Reports that can be queue dependency analyzed.")
    queue_fields = ListOption('analyze',
                              'queue_fields',
                              default=[],
                              doc="Ticket fields that define each queue.")
    audit = ChoiceOption(
        'queues',
        'audit',
        choices=['log', 'ticket', 'none'],
        doc="Record reorderings in log, in ticket, or not at all.")

    def can_analyze(self, report):
        # fallback to actual queue report list if not made explicit
        return report in (self.reports1 or self.reports2)

    def _add_args(self, args, report):
        """Split queue fields into standard and custom."""
        queue_fields = self.env.config.get(
            'analyze', 'queue_fields.' + report,
            self.queue_fields)  # fallback if not report-specific
        if not isinstance(queue_fields, list):
            queue_fields = [f.strip() for f in queue_fields.split(',')]
        args['standard_fields'] = {}
        args['custom_fields'] = {}
        for name in queue_fields:
            vals = None
            if '=' in name:
                name, vals = name.split('=', 1)
                not_ = name.endswith('!')
                if not_:
                    name = name[:-1]
                # save 'not' info at end of vals to pop off later
                vals = [v.strip() for v in vals.split('|')] + [not_]
            for field in TicketSystem(self.env).get_ticket_fields():
                if name == field['name']:
                    if 'custom' in field:
                        args['custom_fields'][name] = vals
                    else:
                        args['standard_fields'][name] = vals
                    break
            else:
                raise Exception("Unknown queue field: %s" % name)

    def get_solutions(self, db, args, report):
        if not args['col1_value1']:
            return '', []  # has no position so skip
        self._add_args(args, report)
        return queue.get_dependency_solutions(db, args)

    def fix_issue(self, db, data, author):
        """Honor queues audit config."""

        if not isinstance(data, list):
            data = [data]

        # find position field
        for k, v in data[0].items():
            if k == 'ticket':
                continue
            field = k
            if self.audit == 'ticket' or \
               field in ('blocking','blockedby') or \
               any(len(c) != 2 for c in data) or \
               not self._isint(v): # heuristic for position field
                return Analysis.fix_issue(self, db, data, author)

        # honor audit config
        cursor = db.cursor()
        for changes in data:
            id = changes['ticket']
            new_pos = changes[field]
            cursor.execute(
                """
                SELECT value from ticket_custom
                 WHERE name=%s AND ticket=%s
                """, (field, id))
            result = cursor.fetchone()
            if result:
                old_pos = result[0]
                cursor.execute(
                    """
                    UPDATE ticket_custom SET value=%s
                     WHERE name=%s AND ticket=%s
                    """, (new_pos, field, id))
            else:
                old_pos = '(none)'
                cursor.execute(
                    """
                    INSERT INTO ticket_custom (ticket,name,value)
                     VALUES (%s,%s,%s)
                    """, (id, field, new_pos))
            if self.audit == 'log':
                self.log.info("%s reordered ticket #%s's %s from %s to %s" \
                    % (author,id,field,old_pos,new_pos))
        db.commit()
示例#13
0
class CkIntegrationModule(Component):
    """CKEditor integration for Trac
    
    Replace wiki-textareas in Trac with the CKEditor, as a fully-featured rich editor.
    
    Adds a request handler for AJAX-based TracWiki->HTML rendering.
    
    The plugin supports several modes of integration, determined by the 
    `editor_type` option (see Configuration section).
    
    The CKEditor itself is not built into the plugin, in order to allow the administrator
    to choose the layout and configuration freely ('''note that CKEditor >= 3.6 is required''').
    Use the `editor_source` option to determine the actual location of the editor.
    
    '''Disclaimer:''' This plugin is under development, and the `full_integration` mode
    is known to be experimental (at best) - only a handful of elements are supported.
    Feel free to join the effort to enhance the `full_integration` at
    http://trac-hacks.org/wiki/CkEditorPlugin.
    
    Configuration (config name, description, default values):
    [[TracIni(ckeditor)]]"""
    implements(ITemplateProvider, ITemplateStreamFilter, IRequestHandler)
    
    editor_type = ChoiceOption('ckeditor', 'editor_type',
        ['full_integration', 'only_ticket', 'only_wiki', 'html_wrapper', 'none'],
        """Type of integrated editor. Possible types are: 
`full_integration`: CKEditor with TracWiki output ('''experimental'''), 
`only_ticket`: CKEditor with TracWiki output for ticket fields ('''experimental'''); ''leaves wiki editing as in Trac standard'', 
`only_wiki`: CKEditor with TracWiki output for wiki pages ('''experimental'''); ''leaves ticket editing as in Trac standard'',
`html_wrapper`: CKEditor with HTML output wrapped in html-processor,  
`none`: No integration - ''leaves editing as in Trac standard''""")
    
    editor_source = Option('ckeditor', 'editor_source', 'site/js/ckeditor/ckeditor.js',
        """Path to CKEditor 3.6.x javascript source.
        
        The path should be specified as viewed by the client,
        and must be accessible by the client-browser.
        
        A recommended setup involves installing CKEditor in the htdocs/js directory
        of the Trac environment, and setting this option to site/js/ckeditor/ckeditor.js.""")
    
    code_styles = ListOption('ckeditor', 'code_styles', 'cpp, csharp, java, js, python, sql, default, xml',
        doc="""List of code styles, which should be processed by CKEditor and 
        displayed in CKEditor dialog 'insert code'.""")
    
#    editor_replace = Option('ckeditor', 'editor_replace', '',
#        """Javascript, which should replace textareas.""")
    
    template_fields = {
        'ticket.html': ('field_description', 'comment', ),
        'wiki_edit.html': ('text', ),
        'admin_components.html': ('description', ),
        'admin_milestones.html': ('description', ),
        'admin_versions.html': ('description', ),
        }

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

    # ITemplateProvider
    def get_templates_dirs(self):
        return []

    def _check_editor_type(self, filename):
        """Checks whether editor is enabled for this view (filename).
        Returns `true` if it is enabled, otherwise `false`.
"""
        if not self.editor_type or 'none' == self.editor_type:
            return False
        elif 'only_ticket' == self.editor_type:
            return lower(filename) == 'ticket.html'
        elif 'only_wiki' == self.editor_type:
            return lower(filename) == 'wiki_edit.html'
        else:
            return lower(filename) in self.template_fields
    
    def get_styles_list(self):
        style_list = [ ]
        if self.code_styles:
            style_opt_list = self.code_styles
            self.log.info('self.code_styles: %s' % style_opt_list)
            for style in style_opt_list:
                if style == 'default':
                    style_list.append(['Text', ''])
                    continue
                
                try:
                    from pygments.lexers import get_lexer_by_name    
                    lexer = get_lexer_by_name(style)
                    style_list.append([lexer.name, style])
                except Exception, e:
                    self.log.warn( "Error when retrieving lexer by name: %s" % e )
        return style_list
示例#14
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
示例#15
0
class MasterTicketsModule(Component):
    """Provides support for ticket dependencies."""

    implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter,
               ITemplateProvider, ITicketManipulator)

    dot_path = Option('mastertickets',
                      'dot_path',
                      default='dot',
                      doc='Path to the dot executable.')
    gs_path = Option('mastertickets',
                     'gs_path',
                     default='gs',
                     doc='Path to the ghostscript executable.')
    use_gs = BoolOption(
        'mastertickets',
        'use_gs',
        default=False,
        doc='If enabled, use ghostscript to produce nicer output.')

    closed_color = Option('mastertickets',
                          'closed_color',
                          default='green',
                          doc='Color of closed tickets')
    opened_color = Option('mastertickets',
                          'opened_color',
                          default='red',
                          doc='Color of opened tickets')
    show_key = Option('mastertickets',
                      'show_key',
                      default=False,
                      doc='Show a key for open/closed nodes')
    closed_text = Option('mastertickets',
                         'closed_text',
                         default='Done',
                         doc='Text for key showing closed tickets')
    opened_text = Option('mastertickets',
                         'opened_text',
                         default='ToDo',
                         doc='Text for key showing opened tickets')
    highlight_target = Option('mastertickets',
                              'highlight_target',
                              default=False,
                              doc='Highlight target tickets in graph')
    full_graph = Option(
        'mastertickets',
        'full_graph',
        default=False,
        doc='Show full dep. graph, not just direct blocking links')

    graph_direction = ChoiceOption(
        'mastertickets',
        'graph_direction',
        choices=['TD', 'LR', 'DT', 'RL'],
        doc=
        'Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)'
    )

    fields = set(['blocking', 'blockedby'])

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.path_info.startswith('/ticket/'):
            # In case of an invalid ticket, the data is invalid
            if not data:
                return template, data, content_type
            tkt = data['ticket']
            links = TicketLinks(self.env, tkt)

            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    add_script(req, 'mastertickets/disable_resolve.js')
                    break

            # Add link to depgraph if needed
            if links:
                add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id))

            for change in data.get('changes', {}):
                if not change.has_key('fields'):
                    continue
                for field, field_data in change['fields'].iteritems():
                    if field in self.fields:
                        if field_data['new'].strip():
                            new = set(
                                [int(n) for n in field_data['new'].split(',')])
                        else:
                            new = set()
                        if field_data['old'].strip():
                            old = set(
                                [int(n) for n in field_data['old'].split(',')])
                        else:
                            old = set()
                        add = new - old
                        sub = old - new
                        elms = tag()
                        if add:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(add)])))
                            elms.append(u' added')
                        if add and sub:
                            elms.append(u'; ')
                        if sub:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(sub)])))
                            elms.append(u' removed')
                        field_data['rendered'] = elms

        #add a link to generate a dependency graph for all the tickets in the milestone
        if req.path_info.startswith('/milestone/'):
            if not data:
                return template, data, content_type
            milestone = data['milestone']
            add_ctxtnav(req, 'Depgraph',
                        req.href.depgraph('milestone', milestone.name))

        return template, data, content_type

    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        if not data:
            return stream

        # We try all at the same time to maybe catch also changed or processed templates
        if filename in [
                "report_view.html", "query_results.html", "ticket.html",
                "query.html"
        ]:
            # For ticket.html
            if 'fields' in data and isinstance(data['fields'], list):
                for field in data['fields']:
                    for f in self.fields:
                        if field['name'] == f and data['ticket'][f]:
                            field['rendered'] = self._link_tickets(
                                req, data['ticket'][f])
            # For query_results.html and query.html
            if 'groups' in data and isinstance(data['groups'], list):
                for group, tickets in data['groups']:
                    for ticket in tickets:
                        for f in self.fields:
                            if f in ticket:
                                ticket[f] = self._link_tickets(req, ticket[f])
            # For report_view.html
            if 'row_groups' in data and isinstance(data['row_groups'], list):
                for group, rows in data['row_groups']:
                    for row in rows:
                        if 'cell_groups' in row and isinstance(
                                row['cell_groups'], list):
                            for cells in row['cell_groups']:
                                for cell in cells:
                                    # If the user names column in the report differently (blockedby AS "blocked by") then this will not find it
                                    if cell.get('header',
                                                {}).get('col') in self.fields:
                                        cell['value'] = self._link_tickets(
                                            req, cell['value'])
        return stream

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

    def validate_ticket(self, req, ticket):
        if req.args.get('action') == 'resolve' and req.args.get(
                'action_resolve_resolve_resolution') == 'fixed':
            links = TicketLinks(self.env, ticket)
            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    yield None, 'Ticket #%s is blocking this ticket' % i

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return [('mastertickets', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        return [resource_filename(__name__, 'templates')]

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/depgraph')

    def process_request(self, req):
        path_info = req.path_info[10:]

        if not path_info:
            raise TracError('No ticket specified')

        #list of tickets to generate the depgraph for
        tkt_ids = []
        milestone = None
        split_path = path_info.split('/', 2)

        #Urls to generate the depgraph for a ticket is /depgraph/ticketnum
        #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name
        if split_path[0] == 'milestone':
            #we need to query the list of tickets in the milestone
            milestone = split_path[1]
            query = Query(self.env,
                          constraints={'milestone': [milestone]},
                          max=0)
            tkt_ids = [fields['id'] for fields in query.execute()]
        else:
            #the list is a single ticket
            tkt_ids = [int(split_path[0])]

        #the summary argument defines whether we place the ticket id or
        #it's summary in the node's label
        label_summary = 0
        if 'summary' in req.args:
            label_summary = int(req.args.get('summary'))

        g = self._build_graph(req, tkt_ids, label_summary=label_summary)
        if path_info.endswith('/depgraph.png') or 'format' in req.args:
            format = req.args.get('format')
            if format == 'text':
                #in case g.__str__ returns unicode, we need to convert it in ascii
                req.send(
                    to_unicode(g).encode('ascii', 'replace'), 'text/plain')
            elif format == 'debug':
                import pprint
                req.send(
                    pprint.pformat(
                        [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]),
                    'text/plain')
            elif format is not None:
                req.send(g.render(self.dot_path, format), 'text/plain')

            if self.use_gs:
                ps = g.render(self.dot_path, 'ps2')
                gs = subprocess.Popen([
                    self.gs_path, '-q', '-dTextAlphaBits=4',
                    '-dGraphicsAlphaBits=4', '-sDEVICE=png16m',
                    '-sOutputFile=%stdout%', '-'
                ],
                                      stdin=subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
                img, err = gs.communicate(ps)
                if err:
                    self.log.debug('MasterTickets: Error from gs: %s', err)
            else:
                img = g.render(self.dot_path)
            req.send(img, 'image/png')
        else:
            data = {}

            #add a context link to enable/disable labels in nodes
            if label_summary:
                add_ctxtnav(req, 'Without labels',
                            req.href(req.path_info, summary=0))
            else:
                add_ctxtnav(req, 'With labels',
                            req.href(req.path_info, summary=1))

            if milestone is None:
                tkt = Ticket(self.env, tkt_ids[0])
                data['tkt'] = tkt
                add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id,
                            req.href.ticket(tkt.id))
            else:
                add_ctxtnav(req, 'Back to Milestone %s' % milestone,
                            req.href.milestone(milestone))
            data['milestone'] = milestone
            data['graph'] = g
            data['graph_render'] = partial(g.render, self.dot_path)
            data['use_gs'] = self.use_gs

            return 'depgraph.html', data, None

    def _build_graph(self, req, tkt_ids, label_summary=0):
        g = graphviz.Graph()
        g.label_summary = label_summary

        g.attributes['rankdir'] = self.graph_direction

        node_default = g['node']
        node_default['style'] = 'filled'

        edge_default = g['edge']
        edge_default['style'] = ''

        # Force this to the top of the graph
        for id in tkt_ids:
            g[id]

        if self.show_key:
            g[-1]['label'] = self.closed_text
            g[-1]['fillcolor'] = self.closed_color
            g[-1]['shape'] = 'box'
            g[-2]['label'] = self.opened_text
            g[-2]['fillcolor'] = self.opened_color
            g[-2]['shape'] = 'box'

        links = TicketLinks.walk_tickets(self.env,
                                         tkt_ids,
                                         full=self.full_graph)
        links = sorted(links, key=lambda link: link.tkt.id)
        for link in links:
            tkt = link.tkt
            node = g[tkt.id]
            if label_summary:
                node['label'] = u'#%s %s' % (tkt.id, tkt['summary'])
            else:
                node['label'] = u'#%s' % tkt.id
            node['fillcolor'] = tkt[
                'status'] == 'closed' and self.closed_color or self.opened_color
            node['URL'] = req.href.ticket(tkt.id)
            node['alt'] = u'Ticket #%s' % tkt.id
            node['tooltip'] = tkt['summary']
            if self.highlight_target and tkt.id in tkt_ids:
                node['penwidth'] = 3

            for n in link.blocking:
                node > g[n]

        return g

    def _link_tickets(self, req, tickets):
        items = []

        for i, word in enumerate(re.split(r'([;,\s]+)', tickets)):
            if i % 2:
                items.append(word)
            elif word:
                ticketid = word
                word = '#%s' % word

                try:
                    ticket = Ticket(self.env, ticketid)
                    if 'TICKET_VIEW' in req.perm(ticket.resource):
                        word = \
                            tag.a(
                                '#%s' % ticket.id,
                                class_=ticket['status'],
                                href=req.href.ticket(int(ticket.id)),
                                title=shorten_line(ticket['summary'])
                            )
                except ResourceNotFound:
                    pass

                items.append(word)

        if items:
            return tag(items)
        else:
            return None
示例#16
0
class RequestDispatcher(Component):
    """Web request dispatcher.

    This component dispatches incoming requests to registered
    handlers.  Besides, 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`.""")

    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. (''since 0.12.1'')
        """)

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

    # 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)

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

        try:
            try:
                # Select the component that should handle the request
                chosen_handler = None
                try:
                    for handler in self._request_handlers.values():
                        if handler.match_request(req):
                            chosen_handler = handler
                            break
                    if not chosen_handler and \
                            (not req.path_info or req.path_info == '/'):
                        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)
                except TracError as e:
                    raise HTTPInternalError(e)
                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:
                    if len(resp) == 2:  # old Clearsilver template and HDF data
                        self.log.error(
                            "Clearsilver template are no longer "
                            "supported (%s)", resp[0])
                        raise TracError(
                            _("Clearsilver templates are no longer supported, "
                              "please contact your Trac administrator."))
                    # Genshi
                    template, data, content_type, method = \
                        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(data, out)
                        req.send(out.getvalue(), 'text/plain')
                    self.log.debug("Rendering response from handler")
                    output = chrome.render_template(
                        req,
                        template,
                        data,
                        content_type,
                        method=method,
                        iterable=chrome.use_chunked_encoding)
                    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:
                # post-process the request in case of errors
                err = sys.exc_info()
                try:
                    self._post_process_request(req)
                except RequestDone:
                    raise
                except Exception as e:
                    self.log.error(
                        "Exception caught while post-processing"
                        " request: %s", exception_to_unicode(e,
                                                             traceback=True))
                raise err[0], err[1], err[2]
        except PermissionError as e:
            raise HTTPForbidden(e)
        except ResourceNotFound as e:
            raise HTTPNotFound(e)
        except TracError as e:
            raise HTTPInternalError(e)

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

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

    # Internal methods

    @lazy
    def _request_handlers(self):
        return dict(
            (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:
            self.log.error("can't retrieve session: %s",
                           exception_to_unicode(e))
            return FakeSession()

    def _get_locale(self, req):
        if has_babel:
            preferred = req.session.get('language')
            default = self.env.config.get('trac', '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'] = 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 req.outcookie['trac_form_token'].value

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

    # RFC7230 3.2 Header Fields
    _xsendfile_header_re = re.compile(r"[-0-9A-Za-z!#$%&'*+.^_`|~]+\Z")
    _warn_xsendfile_header = False

    def _get_xsendfile_header(self, req):
        header = self.xsendfile_header.strip()
        if self._xsendfile_header_re.match(header):
            return to_utf8(header)
        else:
            if not self._warn_xsendfile_header:
                self._warn_xsendfile_header = True
                self.log.warn("[trac] xsendfile_header is invalid: '%s'",
                              header)
            return None

    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):
        resp = args
        # `method` is optional in IRequestHandler's response. If not
        # specified, the default value is appended to response.
        if len(resp) == 3:
            resp += (None, )
        nbargs = len(resp)
        for f in reversed(self.filters):
            # As the arity of `post_process_request` has changed since
            # Trac 0.10, only filters with same arity gets passed real values.
            # Errors will call all filters with None arguments,
            # and results will not be not saved.
            extra_arg_count = arity(f.post_process_request) - 1
            if extra_arg_count == nbargs:
                resp = f.post_process_request(req, *resp)
            elif extra_arg_count == nbargs - 1:
                # IRequestFilters may modify the `method`, but the `method`
                # is forwarded when not accepted by the IRequestFilter.
                method = resp[-1]
                resp = f.post_process_request(req, *resp[:-1])
                resp += (method, )
            elif nbargs == 0:
                f.post_process_request(req, *(None, ) * extra_arg_count)
        return resp
示例#17
0
class LDAPStore(Component):
    """An AccountManager backend to use LDAP."""

    host_url = Option('ldap',
                      'host_url',
                      doc='Server URL to use for LDAP authentication')
    base_dn = Option('ldap', 'base_dn', doc='The user search base')
    bind_user = Option('ldap', 'bind_user', doc='LDAP user for searching')
    bind_password = Option('ldap', 'bind_password', doc='LDAP user password')
    search_scope = ChoiceOption('ldap',
                                'search_scope',
                                SEARCH_SCOPES,
                                doc='The ldap search scope: base, onelevel or '
                                'subtree')
    search_filter = Option('ldap',
                           'search_filter',
                           default='(&(objectClass=user)(sAMAccountName=%s))',
                           doc='The ldap search filter template where %%s is '
                           'replace with the username')

    implements(IPasswordStore)

    def check_password(self, user, password):
        self.log.debug('LDAPAuth: Checking password for user %s', user)

        conn = None
        try:
            conn = self._create_ldap_conn()
            if conn is not None and self._authenticate(conn, self.bind_user,
                                                       self.bind_password):
                bind_cn = self._search_user(conn, user)
                if bind_cn is not None and self._authenticate(
                        conn, bind_cn, password):
                    return PASSWORDSTORE_SUCCESS
                else:
                    return PASSWORDSTORE_FAILURE
            else:
                return PASSWORDSTORE_FALLTHROUGH
        except Exception:
            self.log.debug('LDAPAuth: Unexpected error: %s',
                           traceback.format_exc())
        finally:
            if conn is not None:
                conn.unbind_s()

        return PASSWORDSTORE_FALLTHROUGH

    def get_users(self):
        # TODO: investigate how to get LDAP users that successfully logged in
        return []

    def has_user(self, user):
        return False

    def _create_ldap_conn(self):
        """Creates an LDAP connection"""
        self.log.debug('LDAPAuth: Initializing LDAP connection for %s',
                       self.host_url)
        conn = None
        try:
            conn = ldap.initialize(self.host_url)
            if self._should_use_tls():
                self.log.debug('LDAPAuth: starting TLS')
                conn.start_tls_s()
        except ldap.LDAPError, e:
            self.log.debug('LDAPAuth: Could not create connection: %s', e)

        return conn