示例#1
0
    def __get__(self, instance, owner):
        # FIXME: Better handling of recursive imports
        from multiproduct.env import ProductEnvironment

        if instance is None:
            return self
        components = OrderedExtensionsOption.__get__(self, instance, owner)
        env = getattr(instance, 'env', None)
        return [MultiproductPermissionPolicy(env)] + components \
               if isinstance(env, ProductEnvironment) \
               else components
示例#2
0
    def __get__(self, instance, owner):
        # FIXME: Better handling of recursive imports
        from multiproduct.env import ProductEnvironment

        if instance is None:
            return self
        components = OrderedExtensionsOption.__get__(self, instance, owner)
        env = getattr(instance, 'env', None)
        return [MultiproductPermissionPolicy(env)] + components \
               if isinstance(env, ProductEnvironment) \
               else components
示例#3
0
class PullRequestWorkflowProxy(Component):
    """Provides a special workflow for pull requests and forwards others.

    Don't forget to replace the `TicketActionController` in the workflow
    option in the `[ticket]` section in TracIni. Your original
    controller for tickets other than pull requests can be added as
    general_workflow option.
    If there was only the default workflow option before, the lines will
    look like this:
    {{{
    [ticket]
    workflow = PullRequestWorkflowProxy
    general_workflow = ConfigurableTicketWorkflow
    }}}
    """

    implements(ITicketActionController)

    action_controllers = OrderedExtensionsOption(
        'ticket',
        'general_workflow',
        ITicketActionController,
        default='ConfigurableTicketWorkflow',
        include_missing=False,
        doc="""Ordered list of workflow controllers to use for general tickets.
               That is when a ticket is not a pull request.
               """)

    ### ITicketActionController methods
    def get_ticket_actions(self, req, ticket):
        if ticket['type'] != 'pull request':
            items = (controller.get_ticket_actions(req, ticket)
                     for controller in self.action_controllers)
            return chain.from_iterable(items)

        rm = RepositoryManager(self.env)
        repo = rm.get_repository_by_id(ticket['pr_dstrepo'], True)
        srcrepo = rm.get_repository_by_id(ticket['pr_srcrepo'], True)

        current_status = ticket._old.get('status', ticket['status']) or 'new'
        current_owner = ticket._old.get('owner', ticket['owner'])

        actions = []
        actions.append((4, 'leave'))
        if current_status != 'closed' and req.authname in repo.maintainers():
            actions.append((3, 'accept'))
            actions.append((2, 'reject'))
            if not current_owner or repo.maintainers() - set([current_owner]):
                actions.append((1, 'reassign'))
            actions.append((0, 'review'))
        if current_status == 'closed':
            if srcrepo and repo:
                actions.append((0, 'reopen'))
        return actions

    def get_all_status(self):
        items = (controller.get_all_status()
                 for controller in self.action_controllers)
        return list(chain.from_iterable(items)) + ['under review']

    def render_ticket_action_control(self, req, ticket, action):
        if ticket['type'] != 'pull request':
            items = [
                controller.render_ticket_action_control(req, ticket, action)
                for controller in self.action_controllers
            ]
            return chain.from_iterable(self._filter_resolutions(req, items))

        rm = RepositoryManager(self.env)
        repo = rm.get_repository_by_id(ticket['pr_dstrepo'], True)

        current_status = ticket._old.get('status', ticket['status']) or 'new'
        current_owner = ticket._old.get('owner', ticket['owner'])

        control = []
        hints = []
        if action == 'leave':
            control.append(_('as %(status)s ', status=current_status))
            if current_owner:
                hints.append(
                    _("The owner will remain %(current_owner)s",
                      current_owner=current_owner))
            else:
                hints.append(
                    _("The ticket will remain with no owner",
                      owner=current_owner))
        if action == 'accept':
            if repo.has_node('', ticket['pr_srcrev']):
                hints.append(_("The request will be accepted"))
                hints.append(_("Next status will be '%(name)s'",
                               name='closed'))
            else:
                hints.append(
                    _("The changes must be merged into '%(repo)s' "
                      "first",
                      repo=repo.reponame))
        if action == 'reject':
            if not repo.has_node('', ticket['pr_srcrev']):
                hints.append(_("The request will be rejected"))
                hints.append(_("Next status will be '%(name)s'",
                               name='closed'))
            else:
                hints.append(
                    _("The changes are already present in '%(repo)s'",
                      repo=repo.reponame))
        if action == 'reassign':
            maintainers = (set([repo.owner]) | repo.maintainers())
            maintainers -= set([current_owner])
            selected_owner = req.args.get('action_reassign_reassign_owner',
                                          req.authname)
            control.append(
                tag.select([
                    tag.option(
                        x, value=x, selected=(x == selected_owner or None))
                    for x in maintainers
                ],
                           id='action_reassign_reassign_owner',
                           name='action_reassign_reassign_owner'))
            hints.append(
                _(
                    "The owner will be changed from %(old)s to the "
                    "selected user. Next status will be 'assigned'",
                    old=current_owner))
        if action == 'review':
            if current_owner != req.authname:
                hints.append(
                    _(
                        "The owner will be changes from "
                        "%(current_owner)s to %(authname)s",
                        current_owner=current_owner,
                        authname=req.authname))
            hints.append(
                _("Next status will be '%(name)s'", name='under review'))
        if action == 'reopen':
            hints.append(_("The resolution will be deleted"))
            hints.append(_("Next status will be '%(name)s'", name='reopened'))

        return (action, tag(control), '. '.join(hints) + '.')

    def get_ticket_changes(self, req, ticket, action):
        changes = {}
        if ticket['type'] != 'pull request':
            for controller in self.action_controllers:
                changes.update(
                    controller.get_ticket_changes(req, ticket, action))
            return changes
        updated = {}
        if action == 'accept':
            updated['resolution'] = 'accepted'
            updated['status'] = 'closed'
        if action == 'reject':
            updated['resolution'] = 'rejected'
            updated['status'] = 'closed'
        if action == 'reassign':
            updated['owner'] = req.args.get('action_reassign_reassign_owner')
            updated['status'] = 'assigned'
        if action == 'review':
            updated['owner'] = req.authname
            updated['status'] = 'under review'
        if action == 'reopen':
            updated['resolution'] = ''
            updated['status'] = 'reopened'
        return updated

    def apply_action_side_effects(self, req, ticket, action):
        if ticket['type'] != 'pull request':
            items = (controller.get_action_side_effects(req, ticket, action)
                     for controller in self.action_controllers)
            return chain.from_iterable(items)

    ### Private methods
    def _filter_resolutions(self, req, items):
        for item in items:
            if item[0] != 'resolve':
                yield item
                return

            resolutions = [
                val.name for val in Resolution.select(self.env)
                if int(val.value) > 0
            ]
            ts = TicketSystem(self.env)
            selected_option = req.args.get('action_resolve_resolve_resolution',
                                           ts.default_resolution)
            control = tag.select([
                tag.option(x, value=x, selected=(x == selected_option or None))
                for x in resolutions
            ],
                                 id='action_resolve_resolve_resolution',
                                 name='action_resolve_resolve_resolution')

            yield ('resolve', tag_('as %(resolution)s',
                                   resolution=control), item[2])
示例#4
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

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

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

    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 default is `WikiModule`. (''since 0.9'')""")

    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 = Option(
        'trac', 'default_date_format', '',
        """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, 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'
示例#5
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
示例#6
0
class TicketSystem(Component):
    implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager,
               ITicketManipulator)

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

    realm = 'ticket'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # Public API

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

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

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

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

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

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

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

        fields = TicketFieldList()

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

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

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

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

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

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

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

        return fields

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

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

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

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

            fields.append(field)

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

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

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

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

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

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

    # ITicketManipulator methods

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

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

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

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

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

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

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

    # IPermissionRequestor methods

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

    # IWikiSyntaxProvider methods

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

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

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

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

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

    # IResourceManager methods

    def get_resource_realms(self):
        yield self.realm

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

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

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

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

        >>> from trac.ticket.model import Ticket
        >>> t = Ticket(env)
        >>> int(t.insert())
        1
        >>> resource_exists(env, t.resource)
        True
        """
        try:
            id_ = int(resource.id)
        except (TypeError, ValueError):
            return False
        if self.env.db_query("SELECT id FROM ticket WHERE id=%s", (id_, )):
            if resource.version is None:
                return True
            revcount = self.env.db_query(
                """
                SELECT count(DISTINCT time) FROM ticket_change WHERE ticket=%s
                """, (id_, ))
            return revcount[0][0] >= resource.version
        else:
            return False
示例#7
0
文件: mail.py 项目: zxfly/trac
class EmailDistributor(Component):
    """Distributes notification events as emails."""

    implements(INotificationDistributor)

    formatters = ExtensionPoint(INotificationFormatter)
    decorators = ExtensionPoint(IEmailDecorator)

    resolvers = OrderedExtensionsOption(
        'notification',
        'email_address_resolvers',
        IEmailAddressResolver,
        'SessionEmailResolver',
        include_missing=False,
        doc="""Comma separated 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.
        """)

    default_format = Option(
        'notification',
        'default_format.email',
        'text/plain',
        doc="Default format to distribute email notifications.")

    def __init__(self):
        self._charset = create_charset(
            self.config.get('notification', 'mime_encoding'))

    # INotificationDistributor methods

    def transports(self):
        yield 'email'

    def distribute(self, transport, recipients, event):
        if transport != 'email':
            return
        if not self.config.getbool('notification', 'smtp_enabled'):
            self.log.debug("%s skipped because smtp_enabled set to false",
                           self.__class__.__name__)
            return

        formats = {}
        for f in self.formatters:
            for style, realm in f.get_supported_styles(transport):
                if realm == event.realm:
                    formats[style] = f
        if not formats:
            self.log.error("%s No formats found for %s %s",
                           self.__class__.__name__, transport, event.realm)
            return
        self.log.debug(
            "%s has found the following formats capable of "
            "handling '%s' of '%s': %s", self.__class__.__name__, transport,
            event.realm, ', '.join(formats))

        matcher = RecipientMatcher(self.env)
        notify_sys = NotificationSystem(self.env)
        always_cc = set(notify_sys.smtp_always_cc_list)
        addresses = {}
        for sid, auth, addr, fmt in recipients:
            if fmt not in formats:
                self.log.debug("%s format %s not available for %s %s",
                               self.__class__.__name__, fmt, transport,
                               event.realm)
                continue

            if sid and not addr:
                for resolver in self.resolvers:
                    addr = resolver.get_address_for_session(sid, auth) or None
                    if addr:
                        self.log.debug(
                            "%s found the address '%s' for '%s [%s]' via %s",
                            self.__class__.__name__, addr, sid, auth,
                            resolver.__class__.__name__)
                        break
            if sid and auth and not addr:
                addr = sid
            if notify_sys.smtp_default_domain and \
                    not notify_sys.use_short_addr and \
                    addr and matcher.nodomaddr_re.match(addr):
                addr = '%s@%s' % (addr, notify_sys.smtp_default_domain)
            if not addr:
                self.log.debug(
                    "%s was unable to find an address for "
                    "'%s [%s]'", self.__class__.__name__, sid, auth)
            elif matcher.is_email(addr) or \
                    notify_sys.use_short_addr and \
                    matcher.nodomaddr_re.match(addr):
                addresses.setdefault(fmt, set()).add(addr)
                if sid and auth and sid in always_cc:
                    always_cc.discard(sid)
                    always_cc.add(addr)
                elif notify_sys.use_public_cc:
                    always_cc.add(addr)
            else:
                self.log.debug(
                    "%s was unable to use an address '%s' for '%s "
                    "[%s]'", self.__class__.__name__, addr, sid, auth)

        outputs = {}
        failed = []
        for fmt, formatter in formats.iteritems():
            if fmt not in addresses and fmt != 'text/plain':
                continue
            try:
                outputs[fmt] = formatter.format(transport, fmt, event)
            except Exception as e:
                self.log.warning(
                    '%s caught exception while '
                    'formatting %s to %s for %s: %s%s',
                    self.__class__.__name__, event.realm, fmt, transport,
                    formatter.__class__, exception_to_unicode(e,
                                                              traceback=True))
                failed.append(fmt)

        # Fallback to text/plain when formatter is broken
        if failed and 'text/plain' in outputs:
            for fmt in failed:
                addresses.setdefault('text/plain', set()) \
                         .update(addresses.pop(fmt, ()))

        for fmt, addrs in addresses.iteritems():
            self.log.debug("%s is sending event as '%s' to: %s",
                           self.__class__.__name__, fmt, ', '.join(addrs))
            message = self._create_message(fmt, outputs)
            if message:
                addrs = set(addrs)
                cc_addrs = sorted(addrs & always_cc)
                bcc_addrs = sorted(addrs - always_cc)
                self._do_send(transport, event, message, cc_addrs, bcc_addrs)
            else:
                self.log.warning("%s cannot send event '%s' as '%s': %s",
                                 self.__class__.__name__, event.realm, fmt,
                                 ', '.join(addrs))

    def _create_message(self, format, outputs):
        if format not in outputs:
            return None
        message = create_mime_multipart('related')
        maintype, subtype = format.split('/')
        preferred = create_mime_text(outputs[format], subtype, self._charset)
        if format != 'text/plain' and 'text/plain' in outputs:
            alternative = create_mime_multipart('alternative')
            alternative.attach(
                create_mime_text(outputs['text/plain'], 'plain',
                                 self._charset))
            alternative.attach(preferred)
            preferred = alternative
        message.attach(preferred)
        return message

    def _do_send(self, transport, event, message, cc_addrs, bcc_addrs):
        notify_sys = NotificationSystem(self.env)
        smtp_from = notify_sys.smtp_from
        smtp_from_name = notify_sys.smtp_from_name or self.env.project_name
        smtp_replyto = notify_sys.smtp_replyto
        if not notify_sys.use_short_addr and notify_sys.smtp_default_domain:
            if smtp_from and '@' not in smtp_from:
                smtp_from = '%s@%s' % (smtp_from,
                                       notify_sys.smtp_default_domain)
            if smtp_replyto and '@' not in smtp_replyto:
                smtp_replyto = '%s@%s' % (smtp_replyto,
                                          notify_sys.smtp_default_domain)

        headers = {}
        headers['X-Mailer'] = 'Trac %s, by Edgewall Software'\
                              % self.env.trac_version
        headers['X-Trac-Version'] = self.env.trac_version
        headers['X-Trac-Project'] = self.env.project_name
        headers['X-URL'] = self.env.project_url
        headers['X-Trac-Realm'] = event.realm
        headers['Precedence'] = 'bulk'
        headers['Auto-Submitted'] = 'auto-generated'
        if isinstance(event.target, (list, tuple)):
            targetid = ','.join(map(get_target_id, event.target))
        else:
            targetid = get_target_id(event.target)
        rootid = create_message_id(self.env,
                                   targetid,
                                   smtp_from,
                                   None,
                                   more=event.realm)
        if event.category == 'created':
            headers['Message-ID'] = rootid
        else:
            headers['Message-ID'] = create_message_id(self.env,
                                                      targetid,
                                                      smtp_from,
                                                      event.time,
                                                      more=event.realm)
            headers['In-Reply-To'] = rootid
            headers['References'] = rootid
        headers['Date'] = formatdate()
        headers['From'] = (smtp_from_name, smtp_from) \
                          if smtp_from_name else smtp_from
        headers['To'] = 'undisclosed-recipients: ;'
        if cc_addrs:
            headers['Cc'] = ', '.join(cc_addrs)
        if bcc_addrs:
            headers['Bcc'] = ', '.join(bcc_addrs)
        headers['Reply-To'] = smtp_replyto

        for k, v in headers.iteritems():
            set_header(message, k, v, self._charset)
        for decorator in self.decorators:
            decorator.decorate_message(event, message, self._charset)

        from_name, from_addr = parseaddr(str(message['From']))
        to_addrs = set()
        for name in ('To', 'Cc', 'Bcc'):
            values = map(str, message.get_all(name, ()))
            to_addrs.update(addr for name, addr in getaddresses(values)
                            if addr)
        del message['Bcc']
        notify_sys.send_email(from_addr, list(to_addrs), message.as_string())
示例#8
0
文件: main.py 项目: tsanov/bloodhound
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

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

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

    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 default is `WikiModule`. (''since 0.9'')""")

    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 = Option(
        'trac', 'default_date_format', '',
        """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'')
        """)

    # Public API

    def authenticate(self, req):
        for authenticator in self.authenticators:
            authname = authenticator.authenticate(req)
            if authname:
                return authname
        else:
            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,
        })

        try:
            try:
                # Select the component that should handle the request
                chosen_handler = None
                try:
                    for handler in self.handlers:
                        if handler.match_request(req):
                            chosen_handler = handler
                            break
                    if not chosen_handler:
                        if not req.path_info or req.path_info == '/':
                            chosen_handler = self.default_handler
                    # pre-process any incoming request, whether a handler
                    # was found or not
                    chosen_handler = self._pre_process_request(
                        req, chosen_handler)
                except TracError, e:
                    raise HTTPInternalError(e)
                if not chosen_handler:
                    if req.path_info.endswith('/'):
                        # Strip trailing / and redirect
                        target = req.path_info.rstrip('/').encode('utf-8')
                        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 = \
                              self._post_process_request(req, *resp)
                    if 'hdfdump' in req.args:
                        req.perm.require('TRAC_ADMIN')
                        # debugging helper - no need to render first
                        out = StringIO()
                        pprint(data, out)
                        req.send(out.getvalue(), 'text/plain')

                    output = chrome.render_template(req, template, data,
                                                    content_type)
                    req.send(output, content_type or 'text/html')
                else:
                    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, e:
                    self.log.error(
                        "Exception caught while post-processing"
                        " request: %s", exception_to_unicode(e,
                                                             traceback=True))
                raise err[0], err[1], err[2]
示例#9
0
class EmailDistributor(Component):

    implements(IAnnouncementDistributor, IAnnouncementPreferenceProvider)

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

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

    smtp_debuglevel = IntOption('announcer', 'smtp_debuglevel', 0,
                                """Debug level to pass to smtp python lib""")

    smtp_server = Option(
        'announcer', 'smtp_server', 'localhost',
        """SMTP server hostname to use for email notifications.""")

    smtp_port = IntOption(
        'announcer', 'smtp_port', 25,
        """SMTP server port to use for email notification.""")

    smtp_user = Option('announcer', 'smtp_user', '',
                       """Username for SMTP server. (''since 0.9'').""")

    smtp_password = Option('announcer', 'smtp_password', '',
                           """Password for SMTP server. (''since 0.9'').""")

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

    smtp_ssl = BoolOption('announcer',
                          'smtp_ssl',
                          'false',
                          doc="""Use ssl for smtp connection.""")

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

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

    smtp_always_cc = Option(
        'announcer', 'smtp_always_cc', '',
        """Email address(es) to always send notifications to,
           addresses can be see by all recipients (Cc:).""")

    smtp_always_bcc = Option(
        'announcer', 'smtp_always_bcc', '',
        """Email address(es) to always send notifications to,
           addresses do not appear publicly (Bcc:). (''since 0.10'').""")

    ignore_domains = Option(
        'announcer', 'ignore_domains', '',
        """Comma-separated list of domains that should not be considered
           part of email addresses (for usernames with Kerberos domains)""")

    admit_domains = Option(
        'announcer', 'admit_domains', '',
        """Comma-separated list of domains that should be considered as
        valid for email addresses (such as localdomain)""")

    mime_encoding = Option(
        'announcer', 'mime_encoding', 'base64',
        """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 (''since 0.10'').""")

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

    use_short_addr = BoolOption(
        'announcer', 'use_short_addr', 'false',
        """Permit email address without a host/domain (i.e. username only)
        
        The SMTP server should accept those addresses, and either append
        a FQDN or use local delivery (''since 0.10'').""")

    use_tls = BoolOption(
        'announcer', 'use_tls', 'false',
        """Use SSL/TLS to send notifications (''since 0.10'').""")

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

    smtp_subject_prefix = Option(
        'announcer', 'smtp_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.(''since 0.10.1'').""")

    smtp_to = Option('announcer', 'smtp_to', None, 'Default To: field')

    use_threaded_delivery = BoolOption(
        'announcer', 'use_threaded_delivery', 'false',
        """If true, the actual delivery of the message will occur 
            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',
        doc="""The default mime type of the email notifications.  This
            can be overriden on a per user basis through the announcer
            preferences panel.""")

    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._transmit)
            thread.start()
        return self.delivery_queue

    # IAnnouncementDistributor
    def get_distribution_transport(self):
        return "email"

    def formats(self, transport, realm):
        "Find valid formats for transport and realm"
        formats = {}
        for f in self.formatters:
            if f.get_format_transport() == transport:
                if realm in f.get_format_realms(transport):
                    styles = f.get_format_styles(transport, realm)
                    for style in styles:
                        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):
        if not self.smtp_enabled or \
                transport != self.get_distribution_transport():
            self.log.debug("EmailDistributer smtp_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 = {}
        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.get_format_alternative(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
                msgdict.setdefault(fmt, set()).add((name, authed, addr))
            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])

    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):
        from email.Charset import Charset, QP, BASE64
        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, event, event_id, modtime):
        """Generate a predictable, but sufficiently unique message ID."""
        s = '%s.%s.%d' % (self.env.project_url, event_id, modtime)
        dig = md5(s).hexdigest()
        host = self.smtp_from[self.smtp_from.find('@') + 1:]
        msgid = '<%03d.%s@%s>' % (len(s), dig, host)
        return msgid

    def _event_id(self, event):
        """FIXME: badly needs improvement
        Hacked bullshit.
        """
        if hasattr(event.target, 'id'):
            return "%08d" % event.target.id
        elif hasattr(event.target, 'name'):
            return event.target.name
        else:
            return str(event.target)

    def _do_send(self, transport, event, format, recipients, formatter):
        output = formatter.format(transport, event.realm, format, event)
        subject = formatter.format_subject(transport, event.realm, format,
                                           event)
        alternate_format = formatter.get_format_alternative(
            transport, event.realm, format)
        if alternate_format:
            alternate_output = formatter.format(transport, event.realm,
                                                alternate_format, event)
        else:
            alternate_output = None
        rootMessage = MIMEMultipart("related")
        rootMessage.set_charset(self._charset)
        proj_name = self.env.project_name
        trac_version = get_pkginfo(trac.core).get('version', trac.__version__)
        announcer_version = get_pkginfo(announcerplugin).get(
            'version', 'Undefined')
        rootMessage['X-Mailer'] = 'AnnouncerPlugin v%s on Trac ' \
                'v%s'%(announcer_version, trac_version)
        rootMessage['X-Trac-Version'] = trac_version
        rootMessage['X-Announcer-Version'] = announcer_version
        rootMessage['X-Trac-Project'] = proj_name
        rootMessage['X-Trac-Announcement-Realm'] = event.realm
        event_id = self._event_id(event)
        rootMessage['X-Trac-Announcement-ID'] = event_id
        if self.set_message_id:
            msgid = self._message_id(event, event_id, 0)
            if event.category is not 'created':
                rootMessage['In-Reply-To'] = msgid
                rootMessage['References'] = msgid
                msgid = self._message_id(event, event_id, time.time())
            rootMessage['Message-ID'] = msgid
        rootMessage['Precedence'] = 'bulk'
        rootMessage['Auto-Submitted'] = 'auto-generated'
        provided_headers = formatter.format_headers(transport, event.realm,
                                                    format, event)
        for key in provided_headers:
            rootMessage['X-Announcement-%s'%key.capitalize()] = \
                    to_unicode(provided_headers[key])
        rootMessage['Date'] = formatdate()
        # 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"))

        prefix = self.smtp_subject_prefix
        if prefix == '__default__':
            prefix = '[%s]' % self.env.project_name
        if event.category is not 'created':
            prefix = 'Re: %s' % prefix
        if prefix:
            subject = "%s %s" % (prefix, subject)
        rootMessage['Subject'] = Header(subject, self._charset)
        from_header = '"%s" <%s>' % (Header(self.smtp_from_name or proj_name,
                                            self._charset), self.smtp_from)
        rootMessage['From'] = from_header
        if self.smtp_always_bcc:
            rootMessage['Bcc'] = self.smtp_always_bcc
        if self.smtp_to:
            rootMessage['To'] = '"%s"' % (self.smtp_to)
        if self.use_public_cc:
            rootMessage['Cc'] = ', '.join([x[2] for x in recipients if x])
        rootMessage['Reply-To'] = self.smtp_replyto
        rootMessage.preamble = 'This is a multi-part message in MIME format.'
        if alternate_output:
            parentMessage = MIMEMultipart('alternative')
            rootMessage.attach(parentMessage)
        else:
            parentMessage = rootMessage
        if alternate_output:
            alt_msg_format = 'html' in alternate_format and 'html' or 'plain'
            msgText = MIMEText(alternate_output, alt_msg_format)
            parentMessage.attach(msgText)
        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)
        start = time.time()
        package = (from_header, [x[2] for x in recipients
                                 if x], rootMessage.as_string())
        if self.use_threaded_delivery:
            self.get_delivery_queue().put(package)
        else:
            self._transmit(*package)
        stop = time.time()
        self.log.debug("EmailDistributor took %s seconds to send."\
                %(round(stop-start,2)))

    def _transmit(self, smtpfrom, addresses, message):
        # use defaults to make sure connect() is called in the constructor
        if self.smtp_ssl:
            smtp = smtplib.SMTP_SSL(host=self.smtp_server, port=self.smtp_port)
        else:
            smtp = smtplib.SMTP(host=self.smtp_server, port=self.smtp_port)
        if self.smtp_debuglevel:
            smtp.set_debuglevel(self.smtp_debuglevel)
        if self.use_tls:
            smtp.ehlo()
            if not smtp.esmtp_features.has_key('starttls'):
                raise TracError(_("TLS enabled but server does not support " \
                        "TLS"))
            smtp.starttls()
            smtp.ehlo()
        if self.smtp_user:
            smtp.login(self.smtp_user, self.smtp_password)
        smtp.sendmail(smtpfrom, addresses, message)
        smtp.quit()

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

    def render_announcement_preference_box(self, req, panel):
        transport = self.get_distribution_transport()
        supported_realms = {}
        for formatter in self.formatters:
            if formatter.get_format_transport() == transport:
                for realm in formatter.get_format_realms(transport):
                    if realm not in supported_realms:
                        supported_realms[realm] = set()
                    supported_realms[realm].update(
                        formatter.get_format_styles(transport, realm))
        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
示例#10
0
class HackergotchiModule(Component):
    """A stream filter to add hackergotchi emblems to the timeline."""

    providers = OrderedExtensionsOption(
        'hackergotchi',
        'providers',
        IHackergotchiProvider,
        default='GravatarHackergotchiProvider, IdenticonHackergotchiProvider')

    implements(ITemplateStreamFilter, ITemplateProvider)

    anon_re = re.compile('([^<]+?)\s+<([^>]+)>', re.U)

    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        if req.path_info.startswith('/timeline'):
            closure_state = [0]
            cache = {}

            def f(stream):
                # Update the closed value
                n = closure_state[0]
                closure_state[0] += 1

                # Extract the user information
                author = data['events'][n]['author'].strip()
                user_info = cache.get(author)
                if user_info is not None:
                    author, name, email = user_info
                else:
                    db = self.env.get_db_cnx()
                    user_info = self._get_info(author, db)
                    cache[author] = user_info
                    author, name, email = user_info

                # Try to find a provider
                for provider in self.providers:
                    href = provider.get_hackergotchi(req.href, author, name,
                                                     email)
                    if href is not None:
                        break
                else:
                    href = req.href.chrome('hackergotchi', 'default.png')

                # Build our element
                elm = tag.img(src=href,
                              alt='Hackergotchi for %s' % author,
                              class_='hackergotchi')

                # Output the combined stream
                return itertools.chain(elm.generate(), stream)

            stream |= Transformer(
                '//div[@id="content"]/dl/dt/a/span[@class="time"]').filter(f)
            add_stylesheet(req, 'hackergotchi/hackergotchi.css')
        return stream

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        yield 'hackergotchi', resource_filename(__name__, 'htdocs')

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

    # Internal methods
    def _get_info(self, author, db):
        if author == 'anonymous':
            # Don't even bother trying for "anonymous"
            return author, None, None

        md = self.anon_re.match(author)
        if md:
            # name <email>
            return 'anonymous', md.group(1), md.group(2)

        cursor = db.cursor()
        cursor.execute(
            'SELECT name, value FROM session_attribute WHERE sid=%s AND authenticated=%s',
            (author, 1))
        rows = cursor.fetchall()
        if rows:
            # Authenticated user, with session
            name = email = None
            for key, value in rows:
                if key == 'name':
                    name = value
                elif key == 'email':
                    email = value
            if name or email:
                return author, name, email
            else:
                return author, None, None

        # Assume anonymous user from this point on
        if '@' in author:
            # Likely an email address
            return 'anonymous', None, author

        # See if there is a default domain
        domain = self.config.get('notification', 'smtp_default_domain')
        if domain and ' ' not in author:
            return author, None, author + '@' + domain

        return 'anonymous', author, None
示例#11
0
class RelationsSystem(Component):
    PARENT_RELATION_TYPE = 'parent'
    CHILDREN_RELATION_TYPE = 'children'

    changing_listeners = ExtensionPoint(IRelationChangingListener)
    all_validators = ExtensionPoint(IRelationValidator)
    global_validators = OrderedExtensionsOption(
        'bhrelations',
        'global_validators',
        IRelationValidator,
        'NoSelfReferenceValidator, ExclusiveValidator, BlockerValidator',
        include_missing=False,
        doc="""Validators used to validate all relations,
        regardless of their type.""")

    duplicate_relation_type = Option(
        'bhrelations', 'duplicate_relation', 'duplicateof',
        "Relation type to be used with the resolve as duplicate workflow.")

    def __init__(self):
        links, labels, validators, blockers, copy_fields, exclusive = \
            self._parse_config()
        self._links = links
        self._labels = labels
        self._validators = validators
        self._blockers = blockers
        self._copy_fields = copy_fields
        self._exclusive = exclusive

        self.link_ends_map = {}
        for end1, end2 in self.get_ends():
            self.link_ends_map[end1] = end2
            if end2 is not None:
                self.link_ends_map[end2] = end1

    def get_ends(self):
        return self._links

    def add(self,
            source_resource_instance,
            destination_resource_instance,
            relation_type,
            comment=None,
            author=None,
            when=None):
        source = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, source_resource_instance)
        destination = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, destination_resource_instance)
        if relation_type not in self.link_ends_map:
            raise UnknownRelationType(relation_type)
        if when is None:
            when = datetime.now(utc)
        relation = Relation(self.env)
        relation.source = source
        relation.destination = destination
        relation.type = relation_type
        relation.comment = comment
        relation.author = author
        relation.when = when
        self.add_relation(relation)
        return relation

    def get_reverted_relation(self, relation):
        """Return None if relation is one way"""
        other_end = self.link_ends_map[relation.type]
        if other_end:
            return relation.clone_reverted(other_end)

    def add_relation(self, relation):
        self.validate(relation)
        with self.env.db_transaction:
            relation.insert()
            reverted_relation = self.get_reverted_relation(relation)
            if reverted_relation:
                reverted_relation.insert()

            for listener in self.changing_listeners:
                listener.adding_relation(relation)

            from bhrelations.notification import RelationNotifyEmail
            RelationNotifyEmail(self.env).notify(relation)

    def delete(self, relation_id, when=None):
        if when is None:
            when = datetime.now(utc)
        relation = Relation.load_by_relation_id(self.env, relation_id)
        source = relation.source
        destination = relation.destination
        relation_type = relation.type
        with self.env.db_transaction:
            cloned_relation = relation.clone()
            relation.delete()
            other_end = self.link_ends_map[relation_type]
            if other_end:
                reverted_relation = Relation(self.env,
                                             keys=dict(
                                                 source=destination,
                                                 destination=source,
                                                 type=other_end,
                                             ))
                reverted_relation.delete()

            for listener in self.changing_listeners:
                listener.deleting_relation(cloned_relation, when)

            from bhrelations.notification import RelationNotifyEmail
            RelationNotifyEmail(self.env).notify(cloned_relation, deleted=when)

    def delete_resource_relations(self, resource_instance):
        sql = "DELETE FROM " + Relation.get_table_name() + \
              " WHERE source=%s OR destination=%s"
        full_resource_id = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, resource_instance)
        with self.env.db_transaction as db:
            db(sql, (full_resource_id, full_resource_id))

    def _debug_select(self):
        """The method is used for debug purposes"""
        sql = "SELECT id, source, destination, type FROM bloodhound_relations"
        with self.env.db_query as db:
            return [db(sql)]

    def get_relations(self, resource_instance):
        relation_list = []
        for relation in self._select_relations_for_resource_instance(
                resource_instance):
            relation_list.append(
                dict(
                    relation_id=relation.get_relation_id(),
                    destination_id=relation.destination,
                    destination=ResourceIdSerializer.get_resource_by_id(
                        relation.destination),
                    type=relation.type,
                    comment=relation.comment,
                    when=relation.when,
                    author=relation.author,
                ))
        return relation_list

    def _select_relations_for_resource_instance(self, resource):
        resource_full_id = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, resource)
        return self._select_relations(resource_full_id)

    def _select_relations(self, source, resource_type=None):
        #todo: add optional paging for possible umbrella tickets with
        #a lot of child tickets
        where = dict(source=source)
        if resource_type:
            where["type"] = resource_type
            order_by = ["destination"]
        else:
            order_by = ["type", "destination"]
        return Relation.select(self.env, where=where, order_by=order_by)

    def _parse_config(self):
        links = []
        labels = {}
        validators = {}
        blockers = {}
        copy_fields = {}
        exclusive = set()

        config = self.config[RELATIONS_CONFIG_NAME]
        for name in [
                option for option, _ in config.options() if '.' not in option
        ]:
            reltypes = config.getlist(name)
            if not reltypes:
                continue
            if len(reltypes) == 1:
                reltypes += [None]
            links.append(tuple(reltypes))

            custom_validators = self._parse_validators(config, name)
            for rel in filter(None, reltypes):
                labels[rel] = \
                    config.get(rel + '.label') or rel.capitalize()
                blockers[rel] = \
                    config.getbool(rel + '.blocks', default=False)
                if config.getbool(rel + '.exclusive'):
                    exclusive.add(rel)
                validators[rel] = custom_validators

                # <end>.copy_fields may be absent or intentionally set empty.
                # config.getlist() will return [] in either case, so check that
                # the key is present before assigning the value
                cf_key = '%s.copy_fields' % rel
                if cf_key in config:
                    copy_fields[rel] = config.getlist(cf_key)
        return links, labels, validators, blockers, copy_fields, exclusive

    def _parse_validators(self, section, name):
        custom_validators = set('%sValidator' % validator for validator in set(
            section.getlist(name + '.validators', [], ',', True)))
        validators = []
        if custom_validators:
            for impl in self.all_validators:
                if impl.__class__.__name__ in custom_validators:
                    validators.append(impl)
        return validators

    def validate(self, relation):
        """
        Validate the relation using the configured validators. Validation is
        always run on the relation with master type.
        """
        backrel = self.get_reverted_relation(relation)
        if backrel and (backrel.type, relation.type) in self._links:
            relation = backrel

        for validator in self.global_validators:
            validator.validate(relation)

        for validator in self._validators.get(relation.type, ()):
            validator.validate(relation)

    def is_blocker(self, relation_type):
        return self._blockers[relation_type]

    def render_relation_type(self, end):
        return self._labels[end]

    def get_relation_types(self):
        return self._labels

    def find_blockers(self, resource_instance, is_blocker_method):
        # tbd: do we blocker finding to be recursive
        all_blockers = []
        for relation in self._select_relations_for_resource_instance(
                resource_instance):
            if self.is_blocker(relation.type):
                resource = ResourceIdSerializer.get_resource_by_id(
                    relation.destination)
                resource_instance = is_blocker_method(resource)
                if resource_instance is not None:
                    all_blockers.append(resource_instance)
                    # blockers = self._recursive_find_blockers(
                    #     relation, is_blocker_method)
                    # if blockers:
                    #     all_blockers.extend(blockers)
        return all_blockers

    def get_resource_name(self, resource_id):
        resource = ResourceIdSerializer.get_resource_by_id(resource_id)
        return get_resource_shortname(self.env, resource)
示例#12
0
文件: api.py 项目: tsanov/bloodhound
class BloodhoundSearchApi(Component):
    """Implements core indexing functionality, provides methods for
    searching, adding and deleting documents from index.
    """
    implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment)

    def __init__(self, *args, **kwargs):
        import pkg_resources
        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)
        super(BloodhoundSearchApi, self).__init__(*args, **kwargs)

    backend = ExtensionOption(
        'bhsearch',
        'search_backend',
        ISearchBackend,
        'WhooshBackend',
        'Name of the component implementing Bloodhound Search backend \
        interface: ISearchBackend.',
        doc_domain='bhsearch')

    parser = ExtensionOption(
        'bhsearch',
        'query_parser',
        IQueryParser,
        'DefaultQueryParser',
        'Name of the component implementing Bloodhound Search query \
        parser.',
        doc_domain='bhsearch')

    index_pre_processors = OrderedExtensionsOption(
        'bhsearch',
        'index_preprocessors',
        IDocIndexPreprocessor,
        ['SecurityPreprocessor'],
        include_missing=True,
    )
    result_post_processors = ExtensionPoint(IResultPostprocessor)
    query_processors = ExtensionPoint(IQueryPreprocessor)

    index_participants = MultiProductExtensionPoint(IIndexParticipant)

    def query(self,
              query,
              sort=None,
              fields=None,
              filter=None,
              facets=None,
              pagenum=1,
              pagelen=20,
              highlight=False,
              highlight_fields=None,
              context=None):
        """Return query result from an underlying search backend.

        Arguments:
        :param query: query string e.g. “bla status:closed” or a parsed
            representation of the query.
        :param sort: optional sorting
        :param boost: optional list of fields with boost values e.g.
            {“id”: 1000, “subject” :100, “description”:10}.
        :param filter: optional list of terms. Usually can be cached by
            underlying search framework. For example {“type”: “wiki”}
        :param facets: optional list of facet terms, can be field or expression
        :param page: paging support
        :param pagelen: paging support
        :param highlight: highlight matched terms in fields
        :param highlight_fields: list of fields to highlight
        :param context: request context

        :return: result QueryResult
        """
        # pylint: disable=too-many-locals
        self.env.log.debug("Receive query request: %s", locals())

        parsed_query = self.parser.parse(query, context)

        parsed_filters = self.parser.parse_filters(filter)
        # TODO: add query parsers and meta keywords post-parsing

        # TODO: apply security filters

        query_parameters = dict(
            query=parsed_query,
            query_string=query,
            sort=sort,
            fields=fields,
            filter=parsed_filters,
            facets=facets,
            pagenum=pagenum,
            pagelen=pagelen,
            highlight=highlight,
            highlight_fields=highlight_fields,
        )
        for query_processor in self.query_processors:
            query_processor.query_pre_process(query_parameters, context)

        query_result = self.backend.query(**query_parameters)

        for post_processor in self.result_post_processors:
            post_processor.post_process(query_result)

        query_result.debug["api_parameters"] = query_parameters
        return query_result

    def start_operation(self):
        return self.backend.start_operation()

    def rebuild_index(self):
        """Rebuild underlying index"""
        self.log.info('Rebuilding the search index.')
        self.backend.recreate_index()
        with self.backend.start_operation() as operation_context:
            doc = None
            try:
                for participant in self.index_participants:
                    self.log.info(
                        "Reindexing resources provided by %s in product %s" %
                        (participant.__class__.__name__,
                         getattr(participant.env.product, 'name', "''")))
                    docs = participant.get_entries_for_index()
                    for doc in docs:
                        self.log.debug("Indexing document %s:%s/%s" % (
                            doc.get('product'),
                            doc['type'],
                            doc['id'],
                        ))
                        self.add_doc(doc, operation_context)
                self.log.info("Reindexing complete.")
            except Exception, ex:
                self.log.error(ex)
                if doc:
                    self.log.error("Doc that triggers the error: %s" % doc)
                raise
示例#13
0
class EmailDistributor(Component):

    implements(IAnnouncementDistributor)

    formatters = ExtensionPoint(IAnnouncementFormatter)
    decorators = ExtensionPoint(IAnnouncementEmailDecorator)

    resolvers = OrderedExtensionsOption('announcer',
        'email_address_resolvers', IEmailAddressResolver,
        '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.""")

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

    mime_encoding = Option('announcer', 'mime_encoding', 'base64',
        """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_default = 'undisclosed-recipients: ;'
    to = Option('announcer', 'email_to', to_default, '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.
        """)

    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.enigma = None
        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 methods

    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("EmailDistributor email_enabled set to false")
            return
        formats = self.formats(transport, event.realm)
        if not formats:
            self.log.error("EmailDistributor 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, address in recipients:
            fmt = name and \
                  self._get_preferred_format(event.realm, name, authed) or \
                  self._get_default_format()
            old_fmt = fmt
            if fmt not in formats:
                self.log.debug("EmailDistributor 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
                fmt = None
                for f in formats.values():
                    fmt = f.alternative_style_for(
                        transport, event.realm, old_fmt)
                    if fmt:
                        break
            if not fmt:
                self.log.error("EmailDistributor was unable to find a "
                               "formatter for format %s", old_fmt)
                continue
            resolver = None
            if name and not address:
                # figure out what the addr should be if it's not defined
                for resolver in self.resolvers:
                    address = resolver.get_address_for_session(name, authed)
                    if address:
                        break
            if address:
                self.log.debug("EmailDistributor found the address '%s' "
                               "for '%s (%s)' via: %s", address, name,
                               authed and 'authenticated' or
                               'not authenticated',
                               resolver.__class__.__name__)

                # ok, we found an addr, add the message
                # but wait, check for allowed rcpt first, if set
                if rcpt_allow_re.search(address) is not None:
                    # check for local recipients now
                    local_match = rcpt_local_re.search(address)
                    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(address)
                        if pubkey_ids > 0:
                            msgdict_encrypt.setdefault(fmt, set())\
                                .add((name, authed, address))
                            msg_pubkey_ids[len(msg_pubkey_ids):] = pubkey_ids
                            self.log.debug("EmailDistributor got pubkeys "
                                           "for %s: %s", address, pubkey_ids)
                        else:
                            self.log.debug("EmailDistributor dropped %s "
                                           "after missing pubkey with "
                                           "corresponding address %s in any "
                                           "UID", name, address)
                    else:
                        msgdict.setdefault(fmt, set())\
                            .add((name, authed, address))
                        if local_match is not None:
                            self.log.debug("EmailDistributor expected local "
                                           "delivery for %s to: %s", name,
                                           address)
                else:
                    self.log.debug("EmailDistributor dropped %s for not "
                                   "matching allowed recipient pattern %s",
                                   address, 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 formats.get(k):
                continue
            fmt = formats[k]
            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, fmt)
        for k, v in msgdict_encrypt.items():
            if not v or not formats.get(k):
                continue
            fmt = formats[k]
            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, formats[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
        # Format is unified for all subscriptions of a user.
        result = Subscription.find_by_sid_and_distributor(
            self.env, sid, authenticated, 'email')
        if result:
            chosen = result[0]['format']
            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: %(pref)s",
                              pref=pref))

    def _message_id(self, realm):
        """Generate a predictable, but sufficiently unique message ID."""
        modtime = time.time()
        rand = random.randint(0, 32000)
        s = '%s.%d.%d.%s' % (self.env.project_url,
                             modtime, rand,
                             realm.encode('ascii', 'ignore'))
        dig = hashlib.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=None):
        pubkey_ids = pubkey_ids or []
        # Prepare sender for use in IEmailSender component and message header.
        from_header = formataddr(
            (self.from_name and self.from_name or self.env.project_name,
             self.email_from)
        )
        headers = dict()
        headers['Message-ID'] = self._message_id(event.realm)
        headers['Date'] = formatdate()
        headers['From'] = from_header
        headers['Reply-To'] = self.reply_to

        recip_adds = [x[2] for x in recipients if x]

        if self.use_public_cc:
            headers['Cc'] = ', '.join(recip_adds)
        else:
            # Use localized Bcc: hint for default To: content.
            if self.to == self.to_default:
                headers['To'] = _("undisclosed-recipients: ;")
            else:
                headers['To'] = '"%s"' % self.to
                if self.to:
                    recip_adds += [self.to]
        if not recip_adds:
            self.log.debug("EmailDistributor stopped (no recipients).")
            return
        self.log.debug("All email recipients: %s", recip_adds)

        root_message = MIMEMultipart('related')

        # Write header data into message object.
        for k, v in headers.iteritems():
            set_header(root_message, k, v)

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

        # DEVEL: Currently crypto operations work with format text/plain only.
        alternate_output = None
        alternate_style = []
        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 operation successful.")
        else:
            alternate_style = formatter.alternative_style_for(
                transport,
                event.realm,
                format
            )
            if alternate_style:
                alternate_output = formatter.format(
                    transport,
                    event.realm,
                    alternate_style,
                    event
                )

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

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

            alt_msg_format = 'html' in alternate_style and 'html' or 'plain'
            if isinstance(alternate_output, unicode):
                alternate_output = alternate_output.encode('utf-8')
            msg_text = MIMEText(alternate_output, alt_msg_format)
            msg_text.set_charset(self._charset)
            parent_message.attach(msg_text)
        else:
            parent_message = root_message

        msg_format = 'html' in format and 'html' or 'plain'
        if isinstance(output, unicode):
            output = output.encode('utf-8')
        msg_text = MIMEText(output, msg_format)
        del msg_text['Content-Transfer-Encoding']
        msg_text.set_charset(self._charset)
        # According to RFC 2046, the last part of a multipart message is best
        #   and preferred.
        parent_message.attach(msg_text)

        # DEVEL: Decorators can interfere with crypto operation here. Fix it.
        decorators = self._get_decorators()
        if decorators:
            decorator = decorators.pop()
            decorator.decorate_message(event, root_message, decorators)

        package = (from_header, recip_adds, root_message.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[:]
示例#14
0
class AccountManager(Component):
    """The AccountManager component handles all user account management methods
    provided by the IPasswordStore interface.

    The methods will be handled by the underlying password storage
    implementation set in trac.ini with the "account-manager.password_format"
    setting.

    The "account-manager.password_store" may be an ordered list of password
    stores.  If it is a list, then each password store is queried in turn.
    """

    implements(IAccountChangeListener)

    _password_store = OrderedExtensionsOption('account-manager',
                                              'password_store',
                                              IPasswordStore,
                                              include_missing=False)
    _password_format = Option('account-manager', 'password_format')
    stores = ExtensionPoint(IPasswordStore)
    change_listeners = ExtensionPoint(IAccountChangeListener)
    allow_delete_account = BoolOption(
        'account-manager',
        'allow_delete_account',
        True,
        doc="Allow users to delete their own account.")
    force_passwd_change = BoolOption(
        'account-manager',
        'force_passwd_change',
        True,
        doc="Force the user to change password when it's reset.")
    persistent_sessions = BoolOption(
        'account-manager',
        'persistent_sessions',
        False,
        doc="""Allow the user to be remembered across sessions without
            needing to re-authenticate. This is, user checks a
            \"Remember Me\" checkbox and, next time he visits the site,
            he'll be remembered.""")
    refresh_passwd = BoolOption(
        'account-manager',
        'refresh_passwd',
        False,
        doc="""Re-set passwords on successful authentication.
            This is most useful to move users to a new password store or
            enforce new store configuration (i.e. changed hash type),
            but should be disabled/unset otherwise.""")
    verify_email = BoolOption('account-manager',
                              'verify_email',
                              True,
                              doc="Verify the email address of Trac users.")
    username_char_blacklist = Option(
        'account-manager',
        'username_char_blacklist',
        ':[]',
        doc="""Always exclude some special characters from usernames.
            This is enforced upon new user registration.""")

    def __init__(self):
        # bind the 'acct_mgr' catalog to the specified locale directory
        locale_dir = resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)

    # Public API

    def get_users(self):
        users = []
        for store in self._password_store:
            users.extend(store.get_users())
        return users

    def has_user(self, user):
        exists = False
        user = self.handle_username_casing(user)
        for store in self._password_store:
            if store.has_user(user):
                exists = True
                break
            continue
        return exists

    def has_email(self, email):
        """Returns whether a user account with that email address exists.

        Check db directly - email addresses are not backend-specific.
        """
        exists = False
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            """
            SELECT value
              FROM session_attribute
             WHERE authenticated=1 AND name='email' AND value=%s
            """, (email, ))
        for row in cursor:
            exists = True
            break
        return exists

    def email_verified(self, user, email):
        """Returns whether the account and email has been verified.

        Use with care, as it returns the private token string,
        if verification is pending.
        """
        if (self.user_known(user) is False or email is None) or email == '':
            # nothing to check here
            return None
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            """
            SELECT value
              FROM session_attribute
             WHERE sid=%s AND name='email_verification_sent_to'
            """, (user, ))
        for row in cursor:
            self.log.debug('AcctMgr:api:email_verify for user \"' + \
                user + '\", email \"' + str(email) + '\": ' + str(row[0]))
            if row[0] != email:
                # verification has been sent to different email address
                return None
        cursor.execute(
            """
            SELECT value
              FROM session_attribute
             WHERE sid=%s AND name='email_verification_token'
            """, (user, ))
        for row in cursor:
            # verification token still unverified
            self.log.debug('AcctMgr:api:email_verify for user \"' + \
                user + '\", email \"' + str(email) + '\": ' + str(row[0]))
            return row[0]
        return True

    def user_known(self, user):
        """Returns whether the user has ever been authenticated before.
        """
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            """
            SELECT *
              FROM session
             WHERE authenticated=1 AND sid=%s
            """, (user, ))
        for row in cursor:
            return True
        return False

    def last_seen(self, user=None):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        sql = """
            SELECT sid,last_visit
              FROM session
             WHERE authenticated=1
            """
        if user:
            sql += " AND sid=%s"
            cursor.execute(sql, (user, ))
        else:
            cursor.execute(sql)
        # Don't pass over the cursor (outside of scope), only it's content.
        res = []
        for row in cursor:
            res.append(row)
        return not len(res) == 0 and res or None

    def set_password(self, user, password, old_password=None):
        user = self.handle_username_casing(user)
        store = self.find_user_store(user)
        if store and not hasattr(store, 'set_password'):
            raise TracError(
                _("""The authentication backend for user %s does not support
                setting the password.
                """ % user))
        elif not store:
            store = self.get_supporting_store('set_password')
        if store:
            if store.set_password(user, password, old_password):
                self._notify('created', user, password)
            else:
                self._notify('password_changed', user, password)
        else:
            raise TracError(
                _("""None of the IPasswordStore components listed in the
                trac.ini supports setting the password or creating users.
                """))

    def check_password(self, user, password):
        valid = False
        user = self.handle_username_casing(user)
        for store in self._password_store:
            valid = store.check_password(user, password)
            if valid:
                if valid == True and (self.refresh_passwd == True) and \
                        self.get_supporting_store('set_password'):
                    self._maybe_update_hash(user, password)
                break
        return valid

    def delete_user(self, user):
        user = self.handle_username_casing(user)
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        # Delete session attributes, session and any custom permissions
        # set for the user.
        for table in ['session_attribute', 'session', 'permission']:
            key = (table == 'permission') and 'username' or 'sid'
            # Preseed, since variable table and column names are allowed
            # as SQL arguments (security measure agains SQL injections).
            sql = """
                DELETE
                FROM   %s
                WHERE  %s=%%s
                """ % (table, key)
            cursor.execute(sql, (user, ))
        db.commit()
        db.close()
        # Delete from password store
        self.log.debug('deleted user: %s' % user)
        store = self.find_user_store(user)
        if hasattr(store, 'delete_user'):
            if store and store.delete_user(user):
                self._notify('deleted', user)

    def supports(self, operation):
        try:
            stores = self.password_store
        except AttributeError:
            return False
        else:
            if self.get_supporting_store(operation):
                return True
            else:
                return False

    def password_store(self):
        try:
            return self._password_store
        except AttributeError:
            # fall back on old "password_format" option
            fmt = self._password_format
            for store in self.stores:
                config_key = getattr(store, 'config_key', None)
                if config_key is None:
                    continue
                if config_key() == fmt:
                    return [store]
            # if the "password_format" is not set re-raise the AttributeError
            raise

    password_store = property(password_store)

    def get_supporting_store(self, operation):
        """Returns the IPasswordStore that implements the specified operation.

        None is returned if no supporting store can be found.
        """
        supports = False
        for store in self.password_store:
            if hasattr(store, operation):
                supports = True
                break
            continue
        store = supports and store or None
        return store

    def get_all_supporting_stores(self, operation):
        """Returns a list of stores that implement the specified operation"""
        stores = []
        for store in self.password_store:
            if hasattr(store, operation):
                stores.append(store)
            continue
        return stores

    def find_user_store(self, user):
        """Locates which store contains the user specified.

        If the user isn't found in any IPasswordStore in the chain, None is
        returned.
        """
        ignore_auth_case = self.config.getbool('trac', 'ignore_auth_case')
        user_stores = []
        for store in self._password_store:
            userlist = store.get_users()
            user_stores.append((store, userlist))
            continue
        user = self.handle_username_casing(user)
        for store in user_stores:
            if user in store[1]:
                return store[0]
            continue
        return None

    def handle_username_casing(self, user):
        """Enforce lowercase usernames if required.

        Comply with Trac's own behavior, when case-insensitive
        user authentication is set to True.
        """
        ignore_auth_case = self.config.getbool('trac', 'ignore_auth_case')
        return ignore_auth_case and user.lower() or user

    def _maybe_update_hash(self, user, password):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        sql = """
            SELECT  sid
              FROM  session_attribute
            WHERE   sid=%s
                AND name='password_refreshed'
                AND value=1
            """
        cursor.execute(sql, (user, ))
        if cursor.fetchone() is None:
            self.log.debug('refresh password for user: %s' % user)
            store = self.find_user_store(user)
            pwstore = self.get_supporting_store('set_password')
            if pwstore.set_password(user, password) == True:
                # Account re-created according to current settings
                if store and not (store.delete_user(user) == True):
                    self.log.warn("failed to remove old entry for user '%s'" %
                                  user)
            cursor.execute(
                """
                UPDATE  session_attribute
                    SET value='1'
                WHERE   sid=%s
                    AND name='password_refreshed'
                """, (user, ))
            cursor.execute(sql, (user, ))
            if cursor.fetchone() is None:
                cursor.execute(
                    """
                    INSERT INTO session_attribute
                            (sid,authenticated,name,value)
                    VALUES  (%s,1,'password_refreshed',1)
                    """, (user, ))
            db.commit()

    def _notify(self, func, *args):
        func = 'user_' + func
        for l in self.change_listeners:
            getattr(l, func)(*args)

    # IAccountChangeListener methods

    def user_created(self, user, password):
        self.log.info('Created new user: %s' % user)

    def user_password_changed(self, user, password):
        self.log.info('Updated password for user: %s' % user)

    def user_deleted(self, user):
        self.log.info('Deleted user: %s' % user)

    def user_password_reset(self, user, email, password):
        self.log.info('Password reset user: %s, %s' % (user, email))

    def user_email_verification_requested(self, user, token):
        self.log.info('Email verification requested user: %s' % user)
示例#15
0
class RequestDispatcher(Component):
    """Component responsible for dispatching requests to registered handlers."""

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

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

    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
        default is `WikiModule`. (''since 0.9'')""")

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

    # Public API

    def authenticate(self, req):
        for authenticator in self.authenticators:
            authname = authenticator.authenticate(req)
            if authname:
                return authname
        else:
            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 HDF data set 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,
            'hdf': self._get_hdf,
            'perm': self._get_perm,
            'session': self._get_session,
            'tz': self._get_timezone,
            'form_token': self._get_form_token
        })

        try:
            try:
                # Select the component that should handle the request
                chosen_handler = None
                try:
                    for handler in self.handlers:
                        if handler.match_request(req):
                            chosen_handler = handler
                            break
                    if not chosen_handler:
                        if not req.path_info or req.path_info == '/':
                            chosen_handler = self.default_handler
                    # pre-process any incoming request, whether a handler
                    # was found or not
                    chosen_handler = self._pre_process_request(
                        req, chosen_handler)
                except TracError, e:
                    raise HTTPInternalError(e)
                if not chosen_handler:
                    if req.path_info.endswith('/'):
                        # Strip trailing / and redirect
                        target = req.path_info.rstrip('/').encode('utf-8')
                        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:
                        raise HTTPBadRequest('Missing or invalid form token. '
                                             'Do you have cookies enabled?')

                # Process the request and render the template
                resp = chosen_handler.process_request(req)
                if resp:
                    if len(resp) == 2:  # Clearsilver
                        chrome.populate_hdf(req)
                        template, content_type = \
                                  self._post_process_request(req, *resp)
                        # Give the session a chance to persist changes
                        req.session.save()
                        req.display(template, content_type or 'text/html')
                    else:  # Genshi
                        template, data, content_type = \
                                  self._post_process_request(req, *resp)
                        if 'hdfdump' in req.args:
                            req.perm.require('TRAC_ADMIN')
                            # debugging helper - no need to render first
                            from pprint import pprint
                            out = StringIO()
                            pprint(data, out)
                            req.send(out.getvalue(), 'text/plain')
                        else:
                            output = chrome.render_template(
                                req, template, data, content_type)
                            # Give the session a chance to persist changes
                            req.session.save()
                            req.send(output, content_type or 'text/html')
                else:
                    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, e:
                    self.log.error(
                        "Exception caught while post-processing"
                        " request: %s", exception_to_unicode(e,
                                                             traceback=True))
                raise err[0], err[1], err[2]
示例#16
0
class RequestDispatcher(Component):
    """Component responsible for dispatching requests to registered handlers."""

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

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

    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` and `NewticketModule` (''since 0.9'')."""
    )

    # Public API

    def authenticate(self, req):
        for authenticator in self.authenticators:
            authname = authenticator.authenticate(req)
            if authname:
                return authname
        else:
            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 HDF data set and adds the web
        site chrome.
        """
        # FIXME: For backwards compatibility, should be removed in 0.11
        self.env.href = req.href
        # FIXME in 0.11: self.env.abs_href = Href(self.env.base_url)
        self.env.abs_href = req.abs_href

        # Select the component that should handle the request
        chosen_handler = None
        early_error = None
        req.authname = 'anonymous'
        req.perm = NoPermissionCache()
        try:
            if not req.path_info or req.path_info == '/':
                chosen_handler = self.default_handler
            else:
                for handler in self.handlers:
                    if handler.match_request(req):
                        chosen_handler = handler
                        break

            # Attach user information to the request early, so that
            # the IRequestFilter can see it while preprocessing
            if not getattr(chosen_handler, 'anonymous_request', False):
                try:
                    req.authname = self.authenticate(req)
                    req.perm = PermissionCache(self.env, req.authname)
                    req.session = Session(self.env, req)
                    req.form_token = self._get_form_token(req)
                except:
                    req.authname = 'anonymous'
                    req.perm = NoPermissionCache()
                    early_error = sys.exc_info()

            chosen_handler = self._pre_process_request(req, chosen_handler)
        except:
            early_error = sys.exc_info()

        if not chosen_handler and not early_error:
            early_error = (HTTPNotFound('No handler matched request to %s',
                                        req.path_info), None, None)

        # Prepare HDF for the clearsilver template
        try:
            use_template = getattr(chosen_handler, 'use_template', True)
            req.hdf = None
            if use_template:
                chrome = Chrome(self.env)
                req.hdf = HDFWrapper(loadpaths=chrome.get_all_templates_dirs())
                populate_hdf(req.hdf, self.env, req)
                chrome.populate_hdf(req, chosen_handler)
        except:
            req.hdf = None  # revert to sending plaintext error
            if not early_error:
                raise

        if early_error:
            try:
                self._post_process_request(req)
            except Exception, e:
                self.log.exception(e)
            raise early_error[0], early_error[1], early_error[2]

        # Process the request and render the template
        try:
            try:
                # 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:
                        raise HTTPBadRequest('Missing or invalid form token. '
                                             'Do you have cookies enabled?')

                resp = chosen_handler.process_request(req)
                if resp:
                    template, content_type = self._post_process_request(
                        req, *resp)
                    # Give the session a chance to persist changes
                    if req.session:
                        req.session.save()
                    req.display(template, content_type or 'text/html')
                else:
                    self._post_process_request(req)
            except RequestDone:
                raise
            except:
                err = sys.exc_info()
                try:
                    self._post_process_request(req)
                except Exception, e:
                    self.log.exception(e)
                raise err[0], err[1], err[2]
        except PermissionError, e:
            raise HTTPForbidden(to_unicode(e))
示例#17
0
class BloodhoundSearchModule(Component):
    """Main search page"""
    implements(IPermissionRequestor, IRequestHandler, ITemplateProvider,
               IRequestFilter
               #           IWikiSyntaxProvider #todo: implement later
               )

    search_participants = OrderedExtensionsOption(
        'bhsearch',
        'search_participants',
        ISearchParticipant,
        "TicketSearchParticipant, WikiSearchParticipant",
        include_missing=True)

    prefix = "all"
    default_grid_fields = [
        IndexFields.PRODUCT,
        IndexFields.ID,
        IndexFields.TYPE,
        IndexFields.TIME,
        IndexFields.AUTHOR,
        IndexFields.CONTENT,
    ]

    default_facets = ListOption(
        BHSEARCH_CONFIG_SECTION,
        prefix + '_default_facets',
        default=",".join([IndexFields.PRODUCT, IndexFields.TYPE]),
        doc="""Default facets applied to search view of all resources""",
        doc_domain='bhsearch')

    default_view = Option(
        BHSEARCH_CONFIG_SECTION,
        prefix + '_default_view',
        doc="""If true, show grid as default view for specific resource in
            Bloodhound Search results""",
        doc_domain='bhsearch')

    all_grid_fields = ListOption(
        BHSEARCH_CONFIG_SECTION,
        prefix + '_default_grid_fields',
        default=",".join(default_grid_fields),
        doc="""Default fields for grid view for specific resource""",
        doc_domain='bhsearch')

    default_search = BoolOption(
        BHSEARCH_CONFIG_SECTION,
        'is_default',
        default=False,
        doc="""Searching from quicksearch uses bhsearch.""",
        doc_domain='bhsearch')

    redirect_enabled = BoolOption(
        BHSEARCH_CONFIG_SECTION,
        'enable_redirect',
        default=False,
        doc="""Redirect links pointing to trac search to bhsearch""",
        doc_domain='bhsearch')

    global_quicksearch = BoolOption(
        BHSEARCH_CONFIG_SECTION,
        'global_quicksearch',
        default=True,
        doc="""Quicksearch searches all products, even when used
            in product env.""",
        doc_domain='bhsearch')

    query_suggestions_enabled = BoolOption(
        BHSEARCH_CONFIG_SECTION,
        'query_suggestions',
        default=True,
        doc="""Display query suggestions.""",
        doc_domain='bhsearch')

    # IPermissionRequestor methods
    def get_permission_actions(self):
        return [SEARCH_PERMISSION]

    # IRequestHandler methods
    def match_request(self, req):
        return re.match('^%s' % BHSEARCH_URL, req.path_info) is not None

    def process_request(self, req):
        req.perm.assert_permission(SEARCH_PERMISSION)

        if self._is_opensearch_request(req):
            return ('opensearch.xml', {},
                    'application/opensearchdescription+xml')

        request_context = RequestContext(
            self.env,
            req,
            self.search_participants,
            self.default_view,
            self.all_grid_fields,
            self.default_facets,
            self.global_quicksearch,
            self.query_suggestions_enabled,
        )

        if request_context.requires_redirect:
            req.redirect(request_context.parameters.create_href(), True)

        # compatibility with legacy search
        req.search_query = request_context.parameters.query

        query_result = BloodhoundSearchApi(self.env).query(
            request_context.parameters.query,
            pagenum=request_context.page,
            pagelen=request_context.pagelen,
            sort=request_context.sort,
            fields=request_context.fields,
            facets=request_context.facets,
            filter=request_context.query_filter,
            highlight=True,
            context=request_context,
        )

        request_context.process_results(query_result)
        return self._return_data(req, request_context.data)

    def _is_opensearch_request(self, req):
        return req.path_info == BHSEARCH_URL + '/opensearch'

    def _return_data(self, req, data):
        add_stylesheet(req, 'common/css/search.css')
        return 'bhsearch.html', data, None

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        #        return [('bhsearch',
        #                 pkg_resources.resource_filename(__name__, 'htdocs'))]
        return []

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

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        if SEARCH_URLS_RE.match(req.path_info):
            if self.redirect_enabled:
                return self
        return handler

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

        if self.redirect_enabled:
            data['search_handler'] = req.href.bhsearch()
        elif req.path_info.startswith(SEARCH_URL):
            data['search_handler'] = req.href.search()
        elif self.default_search or req.path_info.startswith(BHSEARCH_URL):
            data['search_handler'] = req.href.bhsearch()
        else:
            data['search_handler'] = req.href.search()
        return template, data, content_type
示例#18
0
class MapDashboard(Component):

    implements(IRequestHandler, INavigationContributor)

    ### configuration options
    openlayers_url = Option('geo', 'openlayers_url',
                            'http://openlayers.org/api/2.8-rc2/OpenLayers.js',
                            "URL of OpenLayers JS to use")
    dashboard_tickets = IntOption(
        'geo', 'dashboard_tickets', '6',
        "number of tickets to display on the dashboard map")
    display_cloud = BoolOption(
        'geo', 'display_cloud', 'true',
        "whether to display the cloud on the map dashboard")
    dashboard = ListOption('geo', 'dashboard', 'activeissues',
                           "which viewports to display on the dashboard")
    marker_style = OrderedExtensionsOption(
        'geo',
        'marker_style',
        IMapMarkerStyle,
        '',
        include_missing=False,
        doc="component to use to set feature style")

    def panels(self):
        """return the panel configuration"""
        retval = []

        # XXX ugly hack because self.dashboard doesn't return
        # a list for no apparent reason
        for panel in self.env.config.getlist('geo', 'dashboard'):

            defaults = {'label': panel, 'query': None}
            config = {}
            for key, default in defaults.items():
                config[key] = self.env.config.get('geo', '%s.%s' %
                                                  (panel, key)) or default
            if config['query'] is not None:
                config['id'] = panel
                retval.append(config)
        return retval

    ### methods for IRequestHandler
    """Extension point interface for request handlers."""

    def match_request(self, req):
        """Return whether the handler wants to process the given request."""
        return req.path_info.strip('/') == 'map'

    def process_request(self, req):
        """Process the request. For ClearSilver, return a (template_name,
        content_type) tuple, where `template` is the ClearSilver template to use
        (either a `neo_cs.CS` object, or the file name of the template), and
        `content_type` is the MIME type of the content. For Genshi, return a
        (template_name, data, content_type) tuple, where `data` is a dictionary
        of substitutions for the template.

        For both templating systems, "text/html" is assumed if `content_type` is
        `None`.

        Note that if template processing should not occur, this method can
        simply send the response itself and not return anything.
        """

        # get the GeoTicket component
        assert self.env.is_component_enabled(GeoTicket)
        geoticket = GeoTicket(self.env)

        # add the query script
        add_script(req, 'common/js/query.js')

        # get the panel configuration
        config = self.panels()

        # build the panels
        panels = []
        located_tickets = geoticket.tickets_with_location()
        for panel in config:

            # query the tickets
            query_string = panel['query']
            query = Query.from_string(self.env, query_string)

            # decide the date to sort by
            if query.order == 'time':
                date_to_display = 'time_created'
            else:
                date_to_display = 'time_changed'
            results = query.execute(req)
            n_tickets = len(results)
            results = [
                result for result in results if result['id'] in located_tickets
            ]
            locations = []
            tickets = []
            results = results[:self.dashboard_tickets]
            for result in results:
                ticket = Ticket(self.env, result['id'])
                try:

                    address, (lat, lon) = geoticket.locate_ticket(ticket)
                    content = geoticket.feature_content(req, ticket)

                    # style for the markers
                    style = {}
                    for extension in self.marker_style:
                        style.update(extension.style(ticket, req, **style))
                    style = style or None

                    locations.append({
                        'latitude': lat,
                        'longitude': lon,
                        'style': style,
                        'content': Markup(content)
                    })
                    tickets.append(ticket)
                except GeolocationException:
                    continue

            title = panel['label']
            panels.append({
                'title': title,
                'id': panel['id'],
                'locations': Markup(simplejson.dumps(locations)),
                'tickets': tickets,
                'n_tickets': n_tickets,
                'date_to_display': date_to_display,
                'query_href': query.get_href(req.href)
            })

        # add the tag cloud, if enabled
        cloud = None
        if self.display_cloud:
            if TagCloudMacro is None:
                self.log.warn(
                    "[geo] display_cloud is set but the TagsPlugin is not installed"
                )
            else:
                formatter = Formatter(self.env, Context.from_request(req))
                macro = TagCloudMacro(self.env)
                cloud = macro.expand_macro(formatter, 'TagCloud', '')
                add_stylesheet(req, 'tags/css/tractags.css')
                add_stylesheet(req, 'tags/css/tagcloud.css')

        # compile data for the genshi template
        data = dict(panels=panels,
                    cloud=cloud,
                    openlayers_url=self.openlayers_url)
        return ('mapdashboard.html', data, 'text/html')

    ### methods for INavigationContributor
    """Extension point interface for components that contribute items to the
    navigation.
    """

    def get_active_navigation_item(self, req):
        """This method is only called for the `IRequestHandler` processing the
        request.
        
        It should return the name of the navigation item that should be
        highlighted as active/current.
        """
        return 'map'

    def get_navigation_items(self, req):
        """Should return an iterable object over the list of navigation items to
        add, each being a tuple in the form (category, name, text).
        """
        yield ('mainnav', 'map',
               tag.a('Map', href=req.href.map(), accesskey='M'))
示例#19
0
class AccountManager(Component):
    """The AccountManager component handles all user account management methods
    provided by the IPasswordStore interface.

    The methods will be handled by underlying password storage implementations
    set in trac.ini with the "account-manager.password_store" option.

    The "account-manager.password_store" may be an ordered list of password
    stores, and if so, then each password store is queried in turn.
    """

    implements(IAccountChangeListener, IPermissionRequestor, IRequestFilter)

    change_listeners = ExtensionPoint(IAccountChangeListener)
    # All checks, not only the configured ones (see self.register_checks).
    checks = ExtensionPoint(IAccountRegistrationInspector)
    password_stores = OrderedExtensionsOption(
        'account-manager',
        'password_store',
        IPasswordStore,
        include_missing=False,
        doc=N_("Ordered list of password stores, queried in turn."))
    register_checks = OrderedExtensionsOption(
        'account-manager',
        'register_check',
        IAccountRegistrationInspector,
        default="""BasicCheck, EmailCheck, BotTrapCheck, RegExpCheck,
                 UsernamePermCheck""",
        include_missing=False,
        doc="""Ordered list of IAccountRegistrationInspector's to use for
        registration checks.""")
    # All stores, not only the configured ones (see self.password_stores).
    stores = ExtensionPoint(IPasswordStore)
    allow_delete_account = BoolOption(
        'account-manager',
        'allow_delete_account',
        True,
        doc="Allow users to delete their own account.")
    force_passwd_change = BoolOption(
        'account-manager',
        'force_passwd_change',
        True,
        doc="Force the user to change password when it's reset.")
    persistent_sessions = BoolOption(
        'account-manager',
        'persistent_sessions',
        False,
        doc="""Allow the user to be remembered across sessions without
            needing to re-authenticate. This is, user checks a
            \"Remember Me\" checkbox and, next time he visits the site,
            he'll be remembered.""")
    refresh_passwd = BoolOption(
        'account-manager',
        'refresh_passwd',
        False,
        doc="""Re-set passwords on successful authentication.
            This is most useful to move users to a new password store or
            enforce new store configuration (i.e. changed hash type),
            but should be disabled/unset otherwise.""")
    username_char_blacklist = Option(
        'account-manager',
        'username_char_blacklist',
        ':[]',
        doc="""Always exclude some special characters from usernames.
            This is enforced upon new user registration.""")

    def __init__(self):
        # Bind the 'acct_mgr' catalog to the specified locale directory.
        locale_dir = resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)

    # Public API

    def get_users(self):
        """Get usernames from all active stores.

        Because we allow concurrent active stores, and some stores even don't
        warrant uniqueness within itself, multiple usernames should be
        expected.
        """
        users = []
        for store in self.password_stores:
            users.extend(store.get_users())
        return users

    def has_user(self, user):
        exists = False
        user = self.handle_username_casing(user)
        for store in self.password_stores:
            if store.has_user(user):
                exists = True
                break
            continue
        return exists

    def set_password(self, user, password, old_password=None, overwrite=True):
        user = self.handle_username_casing(user)
        store = self.find_user_store(user)
        if store and not hasattr(store, 'set_password'):
            raise TracError(
                _("""The authentication backend for user %s does not support
                setting the password.
                """ % user))
        elif not store:
            store = self.get_supporting_store('set_password')
        if store:
            try:
                res = store.set_password(user, password, old_password,
                                         overwrite)
            except TypeError:
                # Support former method signature - overwrite unconditionally.
                res = None
                if overwrite or not store.has_user(user):
                    res = store.set_password(user, password, old_password)
            if res:
                self._notify('created', user, password)
            elif not overwrite:
                raise TracError(
                    _("Password for user %s existed, couldn't create." % user))
            else:
                self._notify('password_changed', user, password)
        else:
            raise TracError(
                _("""None of the IPasswordStore components listed in the
                trac.ini supports setting the password or creating users.
                """))
        return res

    def check_password(self, user, password):
        valid = False
        user = self.handle_username_casing(user)
        for store in self.password_stores:
            valid = store.check_password(user, password)
            if valid:
                if valid == True and (self.refresh_passwd == True) and \
                        self.get_supporting_store('set_password'):
                    self._maybe_update_hash(user, password)
                break
        return valid

    def delete_user(self, user):
        user = self.handle_username_casing(user)
        # Delete credentials from password store.
        store = self.find_user_store(user)
        del_method = getattr(store, 'delete_user', None)
        if callable(del_method):
            del_method(user)
        # Delete session attributes, session and any custom permissions
        # set for the user.
        from acct_mgr.model import delete_user
        delete_user(self.env, user)
        self._notify('deleted', user)

    def supports(self, operation):
        try:
            stores = self.password_stores
        except AttributeError:
            return False
        else:
            if self.get_supporting_store(operation):
                return True
            else:
                return False

    def get_supporting_store(self, operation):
        """Returns the IPasswordStore that implements the specified operation.

        None is returned if no supporting store can be found.
        """
        supports = False
        for store in self.password_stores:
            if hasattr(store, operation):
                supports = True
                break
            continue
        store = supports and store or None
        return store

    def get_all_supporting_stores(self, operation):
        """Returns a list of stores that implement the specified operation"""
        stores = []
        for store in self.password_stores:
            if hasattr(store, operation):
                stores.append(store)
            continue
        return stores

    def find_user_store(self, user):
        """Locates which store contains the user specified.

        If the user isn't found in any IPasswordStore in the chain, None is
        returned.
        """
        user_stores = []
        for store in self.password_stores:
            userlist = store.get_users()
            user_stores.append((store, userlist))
            continue
        user = self.handle_username_casing(user)
        for store in user_stores:
            if user in store[1]:
                return store[0]
            continue
        return None

    def handle_username_casing(self, user):
        """Enforce lowercase usernames if required.

        Comply with Trac's own behavior, when case-insensitive
        user authentication is set to True.
        """
        ignore_auth_case = self.config.getbool('trac', 'ignore_auth_case')
        return ignore_auth_case and user.lower() or user

    def validate_account(self, req, create=False):
        """Run configured registration checks.

        Optionally create a new account on success.
        """
        for inspector in self.register_checks:
            inspector.validate_registration(req)
        if create:
            self._create_user(req)

    def _create_user(self, req):
        """Set password and prime a new authenticated Trac session."""
        email = req.args.get('email', '').strip()
        name = req.args.get('name', '').strip()
        username = self.handle_username_casing(
            req.args.get('username', '').strip())
        # Create the user in the configured (primary) password store.
        if self.set_password(username, req.args.get('password'), None, False):
            # Result of a successful account creation request is a made-up
            # authenticated session, that a new user can refer to later on.
            from acct_mgr.model import prime_auth_session, set_user_attribute
            prime_auth_session(self.env, username)
            # Save attributes for the user with reference to that session ID.
            for attribute in ('name', 'email'):
                value = req.args.get(attribute)
                if not value:
                    continue
                set_user_attribute(self.env, username, attribute, value)

    def _maybe_update_hash(self, user, password):
        from acct_mgr.model import get_user_attribute, set_user_attribute
        if get_user_attribute(self.env, user, 1, 'password_refreshed',
                              1) == [0]:
            self.log.debug("Refresh password for user: %s" % user)
            store = self.find_user_store(user)
            pwstore = self.get_supporting_store('set_password')
            if pwstore.set_password(user, password) == True:
                # Account re-created according to current settings.
                if store and not (store.delete_user(user) == True):
                    self.log.warn("Failed to remove old entry for user: %s" %
                                  user)
            set_user_attribute(self.env, user, 'password_refreshed', 1)

    def _notify(self, mod, *args):
        mod = '_'.join(['user', mod])
        for listener in self.change_listeners:
            # Support divergent account change listener implementations too.
            try:
                getattr(listener, mod)(*args)
            except AttributeError:
                self.log.warn(
                    'IAccountChangeListener %s does not support method %s' %
                    (listener.__class__.__name__, mod))

    # IAccountChangeListener methods

    def user_created(self, user, password):
        self.log.info("Created new user: %s" % user)

    def user_id_changed(self, old_uid, new_uid):
        self.log.info("Changed user id: from '%s' to '%s'" %
                      (old_uid, new_uid))

    def user_password_changed(self, user, password):
        self.log.info("Updated password for user: %s" % user)

    def user_deleted(self, user):
        self.log.info("Deleted user: %s" % user)

    def user_password_reset(self, user, email, password):
        self.log.info("Password reset for user: %s, %s" % (user, email))

    def user_email_verification_requested(self, user, token):
        self.log.info("Email verification requested for user: %s" % user)

    def user_registration_approval_required(self, user):
        self.log.info("Registration approval required for user: %s" % user)

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        if not req.session.authenticated or \
                req.perm.has_permission('ACCTMGR_USER_ADMIN'):
            # Permissions for anonymous and admin users remain unchanged.
            return handler
        if 'approval' in req.session:
            # Account approval not granted, remove elevated permissions.
            req.perm = PermissionCache(self.env)
            self.log.debug(
                "AccountManager.pre_process_request: Permissions for '%s' "
                "stripped (account approval %s)" %
                (req.authname, req.session['approval']))
        return handler

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

    # IPermissionRequestor methods

    def get_permission_actions(self):
        action = [
            'ACCTMGR_CONFIG_ADMIN', 'ACCTMGR_USER_ADMIN', 'EMAIL_VIEW',
            'USER_VIEW'
        ]
        actions = [('ACCTMGR_ADMIN', action), action[0],
                   (action[1], action[2:]), action[3]]
        return actions
示例#20
0
class XmppDistributor(Component):
    """Distribute announcements to XMPP clients."""

    implements(IAnnouncementDistributor)

    formatters = ExtensionPoint(IAnnouncementFormatter)

    resolvers = OrderedExtensionsOption(
        'announcer', 'xmpp_resolvers', IAnnouncementAddressResolver,
        'SpecifiedXmppResolver',
        """Comma seperated list of xmpp resolver components in the order
        they will be called.  If an xmpp address is resolved, the remaining
        resolvers will no be called.
        """)

    default_format = Option('announcer', 'default_xmpp_format', 'text/plain',
                            """Default format for xmpp messages.""")

    server = Option(
        'xmpp', 'server', None,
        """XMPP server hostname to use for jabber notifications.""")

    port = IntOption('xmpp', 'port', 5222,
                     """XMPP server port to use for jabber notification.""")

    user = Option('xmpp', 'user', 'trac@localhost',
                  """Sender address to use in xmpp message.""")

    resource = Option('xmpp', 'resource', 'TracAnnouncerPlugin',
                      """Sender resource to use in xmpp message.""")

    password = Option('xmpp', 'password', None,
                      """Password for XMPP server.""")

    use_threaded_delivery = BoolOption(
        'announcer', 'use_threaded_delivery', False,
        """If true, the actual delivery of the message will occur
            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.
            """)

    def __init__(self):
        self.connections = {}
        self.delivery_queue = None
        self.xmpp_format_setting = SubscriptionSetting(self.env, 'xmpp_format',
                                                       self.default_format)

    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 "xmpp"

    def distribute(self, transport, recipients, event):
        self.log.info('XmppDistributor called')
        if transport != 'xmpp':
            return
        fmtdict = self._formats(transport, event.realm)
        if not fmtdict:
            self.log.error("XmppDistributor No formats found for %s %s" %
                           (transport, event.realm))
            return
        msgdict = {}
        for name, authed, addr in recipients:
            fmt = name and \
                self._get_preferred_format(name, event.realm)
            if fmt not in fmtdict:
                self.log.debug(("XmppDistributor 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(
                    "XmppDistributor was unable to find a formatter " +
                    "for format %s" % k)
                continue
            # TODO:  This won't work with multiple distributors
            #rslvr = None
            # 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
            rslvr = SpecifiedXmppResolver(self.env)
            addr = rslvr.get_address_for_name(name, authed)
            if addr:
                self.log.debug("XmppDistributor 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
                msgdict.setdefault(fmt, set()).add((name, authed, addr))
            else:
                self.log.debug("XmppDistributor 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("XmppDistributor is sending event as '%s' to: %s" %
                           (fmt, ', '.join(x[2] for x in v)))
            self._do_send(transport, event, k, v, fmtdict[k])

    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(
            "XmppDistributor has found the following formats capable "
            "of handling '%s' of '%s': %s" %
            (transport, realm, ', '.join(formats.keys())))
        if not formats:
            self.log.error("XmppDistributor is unable to continue " \
                    "without supporting formatters.")
        return formats

    def _get_preferred_format(self, sid, realm=None):
        if realm:
            name = 'xmpp_format_%s' % realm
        else:
            name = 'xmpp_format'
        setting = SubscriptionSetting(self.env, name,
                                      self.xmpp_format_setting.default)
        return self.xmpp_format_setting.get_user_setting(sid)[0]

    def _do_send(self, transport, event, format, recipients, formatter):
        message = formatter.format(transport, event.realm, format, event)

        package = (recipients, message)

        start = time.time()
        if self.use_threaded_delivery:
            self.get_delivery_queue().put(package)
        else:
            self.send(*package)
        stop = time.time()
        self.log.debug("XmppDistributor took %s seconds to send."\
                %(round(stop-start,2)))

    def send(self, recipients, message):
        """Send message to recipients via xmpp."""
        jid = JID(self.user)
        if self.server:
            server = self.server
        else:
            server = jid.getDomain()
        cl = Client(server, port=self.port, debug=[])
        if not cl.connect():
            raise IOError("Couldn't connect to xmpp server %s" % server)
        if not cl.auth(jid.getNode(), self.password, resource=self.resource):
            cl.Connection.disconnect()
            raise IOError("Xmpp auth erro using %s to %s" % (jid, server))
        default_domain = jid.getDomain()
        for recip in recipients:
            cl.send(Message(recip[2], message))
示例#21
0
class PermissionSystem(Component):
    """Permission management sub-system."""

    required = True

    implements(IPermissionRequestor)

    requestors = ExtensionPoint(IPermissionRequestor)
    group_providers = ExtensionPoint(IPermissionGroupProvider)

    store = ExtensionOption(
        'trac', 'permission_store', IPermissionStore, 'DefaultPermissionStore',
        """Name of the component implementing `IPermissionStore`, which is
        used for managing user and group permissions.""")

    policies = OrderedExtensionsOption(
        'trac', 'permission_policies', IPermissionPolicy,
        'DefaultWikiPolicy, DefaultTicketPolicy, DefaultPermissionPolicy, '
        'LegacyAttachmentPolicy', False,
        """List of components implementing `IPermissionPolicy`, in the order
        in which they will be applied. These components manage fine-grained
        access control to Trac resources.""")

    # Number of seconds a cached user permission set is valid for.
    CACHE_EXPIRY = 5
    # How frequently to clear the entire permission cache
    CACHE_REAP_TIME = 60

    def __init__(self):
        self.permission_cache = {}
        self.last_reap = time_now()

    # Public API

    def grant_permission(self, username, action):
        """Grant the user with the given name permission to perform to
        specified action.

        :raises PermissionExistsError: if user already has the permission
                                       or is a member of the group.

        :since 1.3.1: raises PermissionExistsError rather than IntegrityError
        """
        if action.isupper() and action not in self.get_actions():
            raise TracError(_('%(name)s is not a valid action.', name=action))
        elif not action.isupper() and action.upper() in self.get_actions():
            raise TracError(
                _(
                    "Permission %(name)s differs from a defined "
                    "action by casing only, which is not allowed.",
                    name=action))

        try:
            self.store.grant_permission(username, action)
        except self.env.db_exc.IntegrityError:
            if action in self.get_actions():
                raise PermissionExistsError(
                    _("The user %(user)s already has permission %(action)s.",
                      user=username,
                      action=action))
            else:
                raise PermissionExistsError(
                    _("The user %(user)s is already in the group %(group)s.",
                      user=username,
                      group=action))

    def revoke_permission(self, username, action):
        """Revokes the permission of the specified user to perform an
        action."""
        self.store.revoke_permission(username, action)

    def get_actions_dict(self, skip=None):
        """Get all actions from permission requestors as a `dict`.

        The keys are the action names. The values are the additional actions
        granted by each action. For simple actions, this is an empty list.
        For meta actions, this is the list of actions covered by the action.

        :since 1.0.17: added `skip` argument.
        """
        actions = {}
        for requestor in self.requestors:
            if requestor is skip:
                continue
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.setdefault(action[0], []).extend(action[1])
                else:
                    actions.setdefault(action, [])
        return actions

    def get_actions(self, skip=None):
        """Get a list of all actions defined by permission requestors."""
        actions = set()
        for requestor in self.requestors:
            if requestor is skip:
                continue
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.add(action[0])
                else:
                    actions.add(action)
        return sorted(actions)

    def get_groups_dict(self):
        """Get all groups as a `dict`.

        The keys are the group names. The values are the group members.

        :since: 1.1.3
        """
        groups = sorted(
            (p for p in self.get_all_permissions() if not p[1].isupper()),
            key=lambda p: p[1])

        return {
            k: sorted(i[0] for i in list(g))
            for k, g in groupby(groups, key=lambda p: p[1])
        }

    def get_users_dict(self):
        """Get all users as a `dict`.

        The keys are the user names. The values are the actions possessed
        by the user.

        :since: 1.1.3
        """
        perms = sorted(
            (p for p in self.get_all_permissions() if p[1].isupper()),
            key=lambda p: p[0])

        return {
            k: sorted(i[1] for i in list(g))
            for k, g in groupby(perms, key=lambda p: p[0])
        }

    def get_user_permissions(self,
                             username=None,
                             undefined=False,
                             expand_meta=True):
        """Return the permissions of the specified user.

        The return value is a dictionary containing all the actions
        granted to the user mapped to `True`.

        :param undefined: if `True`, include actions that are not defined
            in any of the `IPermissionRequestor` implementations.
        :param expand_meta: if `True`, expand meta permissions.

        :since 1.3.1: added the `undefined` parameter.
        :since 1.3.3: added the `expand_meta` parameter.
        """
        if not username:
            # Return all permissions available in the system
            return dict.fromkeys(self.get_actions(), True)

        # Return all permissions that the given user has
        actions = self.get_actions_dict()
        user_permissions = self.store.get_user_permissions(username) or []
        if expand_meta:
            return {
                p: True
                for p in self.expand_actions(user_permissions)
                if undefined or p in actions
            }
        else:
            return {
                p: True
                for p in user_permissions if undefined or p in actions
            }

    def get_permission_groups(self, username):
        """Return a sorted list of groups that `username` belongs to.

        Groups are recursively expanded such that if `username` is a
        member of `group1` and `group1` is a member of `group2`, both
        `group1` and `group2` will be returned.

        :since: 1.3.3
        """
        user_groups = set()
        for provider in self.group_providers:
            user_groups.update(provider.get_permission_groups(username) or [])

        return sorted(user_groups)

    def get_all_permissions(self):
        """Return all permissions for all users.

        The permissions are returned as a list of (subject, action)
        formatted tuples.
        """
        return self.store.get_all_permissions() or []

    def get_users_with_permission(self, permission):
        """Return all users that have the specified permission.

        Users are returned as a list of user names.
        """
        now = time_now()
        if now - self.last_reap > self.CACHE_REAP_TIME:
            self.permission_cache = {}
            self.last_reap = now
        timestamp, permissions = self.permission_cache.get(
            permission, (0, None))
        if now - timestamp <= self.CACHE_EXPIRY:
            return permissions

        parent_map = {}
        for parent, children in self.get_actions_dict().iteritems():
            for child in children:
                parent_map.setdefault(child, set()).add(parent)

        satisfying_perms = set()

        def append_with_parents(action):
            if action not in satisfying_perms:
                satisfying_perms.add(action)
                for action in parent_map.get(action, ()):
                    append_with_parents(action)

        append_with_parents(permission)

        perms = self.store.get_users_with_permissions(satisfying_perms) or []
        self.permission_cache[permission] = (now, perms)
        return perms

    def expand_actions(self, actions):
        """Helper method for expanding all meta actions."""
        all_actions = self.get_actions_dict()
        expanded_actions = set()

        def expand_action(action):
            if action not in expanded_actions:
                expanded_actions.add(action)
                for a in all_actions.get(action, ()):
                    expand_action(a)

        for a in actions:
            expand_action(a)
        return sorted(expanded_actions)

    def check_permission(self,
                         action,
                         username=None,
                         resource=None,
                         perm=None):
        """Return True if permission to perform action for the given
        resource is allowed.
        """
        if username is None:
            username = '******'
        if resource and resource.realm is None:
            resource = None
        for policy in self.policies:
            decision = policy.check_permission(action, username, resource,
                                               perm)
            if decision is not None:
                self.log.debug("%s %s %s performing %s on %r",
                               policy.__class__.__name__,
                               'allows' if decision else 'denies', username,
                               action, resource)
                return decision
        self.log.debug("No policy allowed %s performing %s on %r", username,
                       action, resource)
        return False

    # IPermissionRequestor methods

    def get_permission_actions(self):
        """Implement the global `TRAC_ADMIN` meta permission.
        """
        actions = self.get_actions(skip=self)
        return [('TRAC_ADMIN', actions)]
示例#22
0
class SubscriptionManager(Component):
    """A class that manages data subscriptions."""

    subscribables = ExtensionPoint(ISubscribable)
    subscribtion_filters = OrderedExtensionsOption(
        'tracforge-client',
        'filters',
        ISubscriptionFilter,
        include_missing=False,
        doc="""Filters for recieved data.""")

    implements(IEnvironmentSetupParticipant)

    # Subscription accessors
    def get_subscribers(self, type, db=None):
        """Get all envs that are subscribed to this env."""
        return self._get_rows(type, 1, db)

    def get_subscriptions(self, type, db=None):
        """Get all envs this env is subscribed to."""
        return self._get_rows(type, 0, db)

    def _get_rows(self, type, direction, db=None):
        db = db or self.env.get_db_cnx()
        cursor = db.cursor()

        cursor.execute(
            'SELECT env FROM tracforge_subscriptions WHERE type = %s AND direction = %s',
            (type, str(direction)))
        for row in cursor:
            yield row[0]

    def get_subscribables(self):
        for source in self.subscribables:
            for x in source.subscribable_types():
                yield x

    # Subscription mutators
    def subscribe_to(self, source, type):
        source_env = open_env(source)
        source_mgr = SubscriptionManager(source_env)

        self._change_subscription('add', source_env.path, type, 0)
        source_mgr._change_subscription('add', self.env.path, type, 1)

    def unsubscribe_from(self, source, type):
        source_env = open_env(source)
        source_mgr = SubscriptionManager(source_env)

        self._change_subscription('delete', source_env.path, type, 0)
        source_mgr._change_subscription('delete', self.env.path, type, 1)

    def _change_subscription(self, action, env, type, direction):
        db = self.env.get_db_cnx()
        cursor = db.cursor()

        if action == 'add':
            cursor.execute(
                'INSERT INTO tracforge_subscriptions (env, type, direction) VALUES (%s,%s,%s)',
                (env, type, direction))
        elif action == 'delete':
            cursor.execute(
                'DELETE FROM tracforge_subscriptions WHERE env = %s AND type = %s AND direction = %s',
                (env, type, direction))
        else:
            raise TracError, 'Unknown subscription operation'

        db.commit()

    # IEnvironmentSetupParticipant methods
    def environment_created(self):
        self.upgrade_environment(self.env.get_db_cnx())

    def environment_needs_upgrade(self, db):
        cursor = db.cursor()
        cursor.execute(
            "SELECT value FROM system WHERE name = 'tracforge_subscriptions'")
        value = cursor.fetchone()
        if not value:
            self.found_db_version = None
            return True
        else:
            self.found_db_version = int(value[0])
            self.log.debug(
                'SubscriptionManager: Found db version %s, current is %s' %
                (self.found_db_version, db_version))
            return self.found_db_version < db_version

    def upgrade_environment(self, db):
        # 0.10 compatibility hack (thanks Alec)
        try:
            from trac.db import DatabaseManager
            db_manager, _ = DatabaseManager(self.env)._get_connector()
        except ImportError:
            db_manager = db

        # Insert the default table
        cursor = db.cursor()
        if self.found_db_version == None:
            cursor.execute(
                "INSERT INTO system (name, value) VALUES ('tracforge_subscriptions', %s)",
                (db_version, ))
        else:
            cursor.execute(
                "UPDATE system SET value = %s WHERE name = 'tracforge_subscriptions'",
                (db_version, ))
            cursor.execute('DROP TABLE tracforge_subscriptions')

        for sql in db_manager.to_sql(default_table):
            cursor.execute(sql)
        db.commit()
示例#23
0
class PermissionSystem(Component):
    """Permission management sub-system."""

    required = True

    implements(IPermissionRequestor)

    requestors = ExtensionPoint(IPermissionRequestor)

    store = ExtensionOption(
        'trac', 'permission_store', IPermissionStore, 'DefaultPermissionStore',
        """Name of the component implementing `IPermissionStore`, which is used
        for managing user and group permissions.""")

    policies = OrderedExtensionsOption(
        'trac', 'permission_policies', IPermissionPolicy,
        'ReadonlyWikiPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy',
        False,
        """List of components implementing `IPermissionPolicy`, in the order
        in which they will be applied. These components manage fine-grained
        access control to Trac resources.""")

    # Number of seconds a cached user permission set is valid for.
    CACHE_EXPIRY = 5
    # How frequently to clear the entire permission cache
    CACHE_REAP_TIME = 60

    def __init__(self):
        self.permission_cache = {}
        self.last_reap = time()

    # Public API

    def grant_permission(self, username, action):
        """Grant the user with the given name permission to perform to
        specified action."""
        if action.isupper() and action not in self.get_actions():
            raise TracError(_('%(name)s is not a valid action.', name=action))
        elif not action.isupper() and action.upper() in self.get_actions():
            raise TracError(
                _(
                    "Permission %(name)s differs from a defined "
                    "action by casing only, which is not allowed.",
                    name=action))

        self.store.grant_permission(username, action)

    def revoke_permission(self, username, action):
        """Revokes the permission of the specified user to perform an
        action."""
        self.store.revoke_permission(username, action)

    def get_actions_dict(self):
        """Get all actions from permission requestors as a `dict`.

        The keys are the action names. The values are the additional actions
        granted by each action. For simple actions, this is an empty list.
        For meta actions, this is the list of actions covered by the action.
        """
        actions = {}
        for requestor in self.requestors:
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.setdefault(action[0], []).extend(action[1])
                else:
                    actions.setdefault(action, [])
        return actions

    def get_actions(self, skip=None):
        """Get a list of all actions defined by permission requestors."""
        actions = set()
        for requestor in self.requestors:
            if requestor is skip:
                continue
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.add(action[0])
                else:
                    actions.add(action)
        return list(actions)

    def get_groups_dict(self):
        """Get all groups as a `dict`.

        The keys are the group names. The values are the group members.

        :since: 1.1.3
        """
        groups = sorted(
            (p for p in self.get_all_permissions() if not p[1].isupper()),
            key=lambda p: p[1])

        return dict((k, sorted(i[0] for i in list(g)))
                    for k, g in groupby(groups, key=lambda p: p[1]))

    def get_users_dict(self):
        """Get all users as a `dict`.

        The keys are the user names. The values are the actions possessed
        by the user.

        :since: 1.1.3
        """
        perms = sorted(
            (p for p in self.get_all_permissions() if p[1].isupper()),
            key=lambda p: p[0])

        return dict((k, sorted(i[1] for i in list(g)))
                    for k, g in groupby(perms, key=lambda p: p[0]))

    def get_user_permissions(self, username=None):
        """Return the permissions of the specified user.

        The return value is a dictionary containing all the actions granted to
        the user mapped to `True`. If an action is missing as a key, or has
        `False` as a value, permission is denied."""
        if not username:
            # Return all permissions available in the system
            return dict.fromkeys(self.get_actions(), True)

        # Return all permissions that the given user has
        actions = self.get_actions_dict()
        permissions = {}

        def expand_meta(action):
            if action not in permissions:
                permissions[action] = True
                for a in actions.get(action, ()):
                    expand_meta(a)

        for perm in self.store.get_user_permissions(username) or []:
            expand_meta(perm)
        return permissions

    def get_all_permissions(self):
        """Return all permissions for all users.

        The permissions are returned as a list of (subject, action)
        formatted tuples."""
        return self.store.get_all_permissions() or []

    def get_users_with_permission(self, permission):
        """Return all users that have the specified permission.

        Users are returned as a list of user names.
        """
        now = time()
        if now - self.last_reap > self.CACHE_REAP_TIME:
            self.permission_cache = {}
            self.last_reap = now
        timestamp, permissions = self.permission_cache.get(
            permission, (0, None))
        if now - timestamp <= self.CACHE_EXPIRY:
            return permissions

        parent_map = {}
        for parent, children in self.get_actions_dict().iteritems():
            for child in children:
                parent_map.setdefault(child, set()).add(parent)

        satisfying_perms = set()

        def append_with_parents(action):
            if action not in satisfying_perms:
                satisfying_perms.add(action)
                for action in parent_map.get(action, ()):
                    append_with_parents(action)

        append_with_parents(permission)

        perms = self.store.get_users_with_permissions(satisfying_perms) or []
        self.permission_cache[permission] = (now, perms)
        return perms

    def expand_actions(self, actions):
        """Helper method for expanding all meta actions."""
        all_actions = self.get_actions_dict()
        expanded_actions = set()

        def expand_action(action):
            if action not in expanded_actions:
                expanded_actions.add(action)
                for a in all_actions.get(action, ()):
                    expand_action(a)

        for a in actions:
            expand_action(a)
        return expanded_actions

    def check_permission(self,
                         action,
                         username=None,
                         resource=None,
                         perm=None):
        """Return True if permission to perform action for the given resource
        is allowed."""
        if username is None:
            username = '******'
        if resource and resource.realm is None:
            resource = None
        for policy in self.policies:
            decision = policy.check_permission(action, username, resource,
                                               perm)
            if decision is not None:
                if decision is False:
                    self.log.debug("%s denies %s performing %s on %r",
                                   policy.__class__.__name__, username, action,
                                   resource)
                return decision
        self.log.debug("No policy allowed %s performing %s on %r", username,
                       action, resource)
        return False

    # IPermissionRequestor methods

    def get_permission_actions(self):
        """Implement the global `TRAC_ADMIN` meta permission.
        """
        actions = self.get_actions(skip=self)
        return [('TRAC_ADMIN', actions)]
示例#24
0
class MilestoneSystem(Component):
    """
    Copy of the TicketSystem for enabling the same functionality for milestones.
    """
    action_controllers = OrderedExtensionsOption(
        'itteco-milestone',
        'workflow',
        IMilestoneActionController,
        default='ConfigurableMilestoneWorkflow',
        include_missing=False,
        doc=
        """Ordered list of workflow controllers to use for milestone actions. Reserved for future use."""
    )

    tickets_report = IntOption(
        'itteco-milestone',
        'tickets_report',
        doc=
        """The number of the report that is to be rendered in in milestone editor."""
    )

    starting_action = ListOption(
        'itteco-milestone',
        'starting_action',
        'start,reassign',
        doc="""List of the actions that mark milestone as started.""")

    completing_action = ListOption(
        'itteco-milestone',
        'completing_action',
        'finish,resolve',
        doc="""List of the actions that mark milestone as started.""")

    _fields = None
    _custom_fields = None

    def __init__(self):
        self.log.debug('action controllers for milestone workflow: %r' %
                       [c.__class__.__name__ for c in self.action_controllers])
        self._fields_lock = threading.RLock()

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

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

    def get_milestone_fields(self):
        """Returns the list of fields available for milestones."""
        # This is now cached - as it makes quite a number of things faster,
        # e.g. #6436
        if self._fields is None:
            self._fields_lock.acquire()
            try:
                if self._fields is None:  # double-check (race after 1st check)
                    self._fields = self._get_milestone_fields()
            finally:
                self._fields_lock.release()
        return [f.copy() for f in self._fields]

    def reset_milestone_fields(self):
        self._fields_lock.acquire()
        try:
            self._fields = None
            self.config.touch()  # brute force approach for now
        finally:
            self._fields_lock.release()

    def _get_milestone_fields(self):
        db = self.env.get_db_cnx()
        fields = [{
            'name': 'summary',
            'type': 'text',
            'label': 'Summary'
        }, {
            'name': 'description',
            'type': 'textarea',
            'label': 'Description'
        }, {
            'name': 'duedate',
            'type': 'text',
            'label': 'Due date',
            'skip': True,
            'custom': True
        }, {
            'name': 'completedate',
            'type': 'text',
            'label': 'Complete date',
            'skip': True,
            'custom': True
        }, {
            'name': 'started',
            'type': 'text',
            'label': 'Started At',
            'skip': True,
            'custom': True
        }, {
            'name': 'type',
            'type': 'text',
            'label': 'TypesAt',
            'skip': True
        }, {
            'name': 'milestone',
            'type': 'text',
            'label': 'Parent',
            'options': []
        }, {
            'name': 'owner',
            'type': 'text',
            'label': 'Owner'
        }, {
            'name': 'reporter',
            'type': 'text',
            'label': 'Reporter',
            'skip': True
        }, {
            'name': 'status',
            'type': 'select',
            'label': 'Status',
            'options': MilestoneSystem(self.env).get_all_status(),
            'hidden': True
        }]
        #put the default fields is any

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

        return fields

    reserved_field_names = [
        'report', 'order', 'desc', 'group', 'groupdesc', 'col', 'row',
        'format', 'max', 'page', 'verbose', 'comment'
    ]

    def get_custom_fields(self):
        if self._custom_fields is None:
            self._fields_lock.acquire()
            try:
                if self._custom_fields is None:  # double-check
                    self._custom_fields = self._get_custom_fields()
            finally:
                self._fields_lock.release()
        return [f.copy() for f in self._custom_fields]

    def _get_custom_fields(self):
        fields = []
        config = self.config['milestone-custom']
        for name in [
                option for option, value in config.options()
                if '.' not in option
        ]:
            field = {
                'name': name,
                'type': config.get(name),
                'custom': True,
                'order': config.getint(name + '.order', 0),
                'label': config.get(name + '.label') or name.capitalize(),
                'value': config.get(name + '.value', '')
            }
            if field['type'] == 'select' or field['type'] == 'radio':
                field['options'] = config.getlist(name + '.options', sep='|')
                if '' in field['options']:
                    field['optional'] = True
                    field['options'].remove('')
            elif field['type'] == 'text':
                field['format'] = config.get(name + '.format', 'plain')
            elif field['type'] == 'textarea':
                field['format'] = config.get(name + '.format', 'plain')
                field['width'] = config.getint(name + '.cols')
                field['height'] = config.getint(name + '.rows')
            fields.append(field)

        fields.sort(lambda x, y: cmp(x['order'], y['order']))
        return fields
示例#25
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
示例#26
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
示例#27
0
class PermissionSystem(Component):
    """Permission management sub-system."""

    required = True

    implements(IPermissionRequestor)

    requestors = ExtensionPoint(IPermissionRequestor)

    store = ExtensionOption(
        'trac', 'permission_store', IPermissionStore, 'DefaultPermissionStore',
        """Name of the component implementing `IPermissionStore`, which is used
        for managing user and group permissions.""")

    policies = OrderedExtensionsOption(
        'trac', 'permission_policies', IPermissionPolicy,
        'DefaultPermissionPolicy, LegacyAttachmentPolicy', False,
        """List of components implementing `IPermissionPolicy`, in the order in
        which they will be applied. These components manage fine-grained access
        control to Trac resources.
        Defaults to the DefaultPermissionPolicy (pre-0.11 behavior) and
        LegacyAttachmentPolicy (map ATTACHMENT_* permissions to realm specific
        ones)""")

    # Number of seconds a cached user permission set is valid for.
    CACHE_EXPIRY = 5
    # How frequently to clear the entire permission cache
    CACHE_REAP_TIME = 60

    def __init__(self):
        self.permission_cache = {}
        self.last_reap = time_now()

    # Public API

    def grant_permission(self, username, action):
        """Grant the user with the given name permission to perform to specified
        action."""
        if action.isupper() and action not in self.get_actions():
            raise TracError(_('%(name)s is not a valid action.', name=action))

        self.store.grant_permission(username, action)

    def revoke_permission(self, username, action):
        """Revokes the permission of the specified user to perform an action."""
        self.store.revoke_permission(username, action)

    def get_actions_dict(self):
        """Get all actions from permission requestors as a `dict`.

        The keys are the action names. The values are the additional actions
        granted by each action. For simple actions, this is an empty list.
        For meta actions, this is the list of actions covered by the action.
        """
        actions = {}
        for requestor in self.requestors:
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.setdefault(action[0], []).extend(action[1])
                else:
                    actions.setdefault(action, [])
        return actions

    def get_actions(self, skip=None):
        """Get a list of all actions defined by permission requestors."""
        actions = set()
        for requestor in self.requestors:
            if requestor is skip:
                continue
            for action in requestor.get_permission_actions() or []:
                if isinstance(action, tuple):
                    actions.add(action[0])
                else:
                    actions.add(action)
        return list(actions)

    def get_user_permissions(self, username=None):
        """Return the permissions of the specified user.

        The return value is a dictionary containing all the actions granted to
        the user mapped to `True`. If an action is missing as a key, or has
        `False` as a value, permission is denied."""
        if not username:
            # Return all permissions available in the system
            return dict.fromkeys(self.get_actions(), True)

        # Return all permissions that the given user has
        actions = self.get_actions_dict()
        permissions = {}

        def expand_meta(action):
            if action not in permissions:
                permissions[action] = True
                for a in actions.get(action, ()):
                    expand_meta(a)

        for perm in self.store.get_user_permissions(username) or []:
            expand_meta(perm)
        return permissions

    def get_all_permissions(self):
        """Return all permissions for all users.

        The permissions are returned as a list of (subject, action)
        formatted tuples."""
        return self.store.get_all_permissions() or []

    def get_users_with_permission(self, permission):
        """Return all users that have the specified permission.

        Users are returned as a list of user names.
        """
        now = time_now()
        if now - self.last_reap > self.CACHE_REAP_TIME:
            self.permission_cache = {}
            self.last_reap = now
        timestamp, permissions = self.permission_cache.get(
            permission, (0, None))
        if now - timestamp <= self.CACHE_EXPIRY:
            return permissions

        parent_map = {}
        for parent, children in self.get_actions_dict().iteritems():
            for child in children:
                parent_map.setdefault(child, set()).add(parent)

        satisfying_perms = set()

        def append_with_parents(action):
            if action not in satisfying_perms:
                satisfying_perms.add(action)
                for action in parent_map.get(action, ()):
                    append_with_parents(action)

        append_with_parents(permission)

        perms = self.store.get_users_with_permissions(satisfying_perms) or []
        self.permission_cache[permission] = (now, perms)
        return perms

    def expand_actions(self, actions):
        """Helper method for expanding all meta actions."""
        all_actions = self.get_actions_dict()
        expanded_actions = set()

        def expand_action(action):
            if action not in expanded_actions:
                expanded_actions.add(action)
                for a in all_actions.get(action, ()):
                    expand_action(a)

        for a in actions:
            expand_action(a)
        return expanded_actions

    def check_permission(self,
                         action,
                         username=None,
                         resource=None,
                         perm=None):
        """Return True if permission to perform action for the given resource
        is allowed."""
        if username is None:
            username = '******'
        if resource:
            if resource.realm is None:
                resource = None
            elif resource.neighborhood is not None:
                try:
                    compmgr = manager_for_neighborhood(self.env,
                                                       resource.neighborhood)
                except ResourceNotFound:
                    # FIXME: raise ?
                    return False
                else:
                    return PermissionSystem(compmgr).check_permission(
                        action, username, resource, perm)
        for policy in self.policies:
            decision = policy.check_permission(action, username, resource,
                                               perm)
            if decision is not None:
                if decision is False:
                    self.log.debug("%s denies %s performing %s on %r",
                                   policy.__class__.__name__, username, action,
                                   resource)
                return decision
        self.log.debug("No policy allowed %s performing %s on %r", username,
                       action, resource)
        return False

    # IPermissionRequestor methods

    def get_permission_actions(self):
        """Implement the global `TRAC_ADMIN` meta permission.

        Implements also the `EMAIL_VIEW` permission which allows for
        showing email addresses even if `[trac] show_email_addresses`
        is `false`.
        """
        actions = self.get_actions(skip=self)
        actions.append('EMAIL_VIEW')
        return [('TRAC_ADMIN', actions), 'EMAIL_VIEW']
示例#28
0
class GeoTicket(Component):

    implements(ICustomFieldProvider, ITicketManipulator, ITicketChangeListener,
               IRequestFilter, IRequestHandler, ITemplateProvider,
               ITemplateStreamFilter, IEnvironmentSetupParticipant)

    ### configuration options
    mandatory_location = BoolOption(
        'geo', 'mandatory_location', 'false',
        "Enforce a mandatory and valid location field")
    google_api_key = Option(
        'geo', 'google_api_key', '',
        "Google maps API key, available at http://code.google.com/apis/maps/signup.html"
    )
    wms_url = Option('geo', 'wms_url',
                     'http://maps.opengeo.org/geowebcache/service/wms',
                     "URL for the WMS")
    openlayers_url = Option('geo', 'openlayers_url',
                            'http://openlayers.org/api/2.8-rc2/OpenLayers.js',
                            "URL of OpenLayers JS to use")

    # default viewing frame lat/lon
    min_lat = FloatOption('geo', 'min_lat', '-85.',
                          "minimum latitude for default map display")
    max_lat = FloatOption('geo', 'max_lat', '85.',
                          "maximum latitude for default map display")
    min_lon = FloatOption('geo', 'min_lon', '-180.',
                          "minimum longitude for default map display")
    max_lon = FloatOption('geo', 'max_lon', '180.',
                          "maximum longitude for default map display")

    # options for customizing map display
    feature_popup = Option('geo', 'feature_popup', '',
                           "template for map feature popup")
    marker_style = OrderedExtensionsOption(
        'geo',
        'marker_style',
        IMapMarkerStyle,
        '',
        include_missing=False,
        doc="components to use to set feature style")

    ### method for ICustomFieldProvider

    # TODO : ensure CustomFieldProvider is enabled
    # XXX or, should CustomFieldProvider be used at all?
    def fields(self):
        return {'location': None}

    ### methods for ITicketManipulator

    def prepare_ticket(self, req, ticket, fields, actions):
        """Not currently called, but should be provided for future
        compatibility."""

    def validate_ticket(self, req, ticket):
        """Validate a ticket after it's been populated from user input.
        
        Must return a list of `(field, message)` tuples, one for each problem
        detected. `field` can be `None` to indicate an overall problem with the
        ticket. Therefore, a return value of `[]` means everything is OK."""

        location_changed = True

        # check for latitude and longitude in the request
        if 'latitude' in req.args and 'longitude' in req.args:
            lat = float(req.args['latitude'])
            lon = float(req.args['longitude'])
        else:
            lat = lon = None
            # compare ticket['location'] with stored version for existing tickets
            if ticket.id:
                location_changed = not ticket['location'] == Ticket(
                    self.env, ticket.id)['location']

        # get location string
        location = ticket['location']
        if location is None:
            location = ''
        location = location.strip()

        # enforce the location field, if applicable
        if not location:
            if location_changed and ticket.id:
                self.delete_location(ticket.id)
            if self.mandatory_location:
                return [('location', 'Please enter a location')]
            else:
                return []

        # do nothing if the location isn't changed
        if not location_changed:
            return []

        # XXX blindly assume UTF-8
        try:
            location = location.encode('utf-8')
        except UnicodeEncodeError:
            raise

        # geolocate the address
        if lat is not None and lon is not None:
            if ticket.id:
                self.set_location(ticket.id, lat, lon)
            else:
                # XXX what if not ticket.id ?
                ticket.latitude = lat
                ticket.longitude = lon
        else:
            try:
                ticket['location'], (lat, lon) = self.geolocate(location)
                if ticket.id:
                    self.set_location(ticket.id, lat, lon)
            except GeolocationException, e:
                if location_changed and ticket.id:
                    self.delete_location(ticket.id)
                if len(e.locations) > 1:
                    return [('location', str(e))]
                if self.mandatory_location:
                    return [('location', str(e))]

                # store the error in a cookie as add_warning is clobbered
                # in the post-POST redirect
                req.session['geolocation_error'] = e.html()
                req.session.save()

        return []