Esempio n. 1
0
class TracCaptchaController(Component):

    implements(IPermissionRequestor)

    captcha = ExtensionOption(
        'trac-captcha', 'captcha', ICaptcha, 'reCAPTCHAImplementation',
        '''Name of the component implementing `ICaptcha`, which is used to 
        generate actual captchas.''')

    stored_token_key = Option(
        'trac-captcha', 'token_key', None,
        '''Generated private key which is used to encrypt captcha tokens.''')

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

    # --- IPermissionRequestor -------------------------------------------------
    def get_permission_actions(self):
        permissions = ['CAPTCHA_SKIP']
        if Version(major=0, minor=13) <= trac_version:
            # enhancing existing meta-permissions is only possible since
            # Trac's r10417 (which is in 0.13), see
            # http://trac.edgewall.org/ticket/8036
            permissions.append(('TICKET_ADMIN', ['CAPTCHA_SKIP']))
        return permissions

    # --- public API -----------------------------------------------------------
    def should_skip_captcha(self, req):
        if 'CAPTCHA_SKIP' in req.perm:
            self.debug_log(
                'Skipping CAPTCHA for %(path)s because of CAPTCHA_SKIP' %
                dict(path=req.path_info))
            return True
        captcha_token = req.args.get('__captcha_token')
        if self.is_token_valid(captcha_token):
            self.debug_log(
                'Skipping CAPTCHA for %(path)s because request has valid token %(token)s'
                % dict(path=req.path_info, token=repr(captcha_token)))
            self.add_token_for_request(req, captcha_token)
            return True
        return False

    def check_captcha_solution(self, req):
        if self.should_skip_captcha(req):
            return None
        try:
            self.captcha.assert_captcha_completed(req)
        except CaptchaFailedError, e:
            self.debug_log(
                'Wrong CAPTCHA solution for %(path)s: %(arguments)s' %
                dict(path=req.path_info, arguments=repr(req.args)))
            req.captcha_data = e.captcha_data
            return e.msg
        self.debug_log(
            'Accepted CAPTCHA solution for %(path)s: %(arguments)s' %
            dict(path=req.path_info, arguments=repr(req.args)))
        self.add_token_for_request(req)
        return None
class NewTicketLikeThisPlugin(Component):

    implements(ITemplateStreamFilter)

    ticket_cloner = ExtensionOption(
        'newticketlikethis', 'ticket_cloner', ITicketCloner,
        'SimpleTicketCloner',
        """Name of the component implementing `ITicketCloner`, which provides the logic for building a new ticket from an existing one."""
    )
    ticket_clone_permission = Option(
        'newticketlikethis', 'ticket_clone_permission', 'TICKET_ADMIN',
        """The permission required for the "Clone" button to appear on the ticket detail page"""
    )
    ticket_clone_form_action = Option(
        'newticketlikethis',
        'ticket_clone_form_action',
        default=None,
        doc=
        "URL to submit the 'ticket clone' form to.  If this is not provided, the current Trac instance's /newticket URL will be used."
    )
    ticket_clone_form_method = Option(
        'newticketlikethis',
        'ticket_clone_form_method',
        default="POST",
        doc="What HTTP method to submit the 'ticket clone' form with")

    # ITemplateStreamFilter methods

    def filter_stream(self, req, method, filename, stream, data):
        if filename == 'ticket.html':
            ticket = data.get('ticket')
            if ticket and ticket.exists and \
                    self.ticket_clone_permission in req.perm(ticket.resource):
                filter = Transformer('//h3[@id="comment:description"]')
                stream |= filter.after(self._clone_form(req, ticket, data))
        return stream

    def _clone_form(self, req, ticket, data):
        fields = self.ticket_cloner.build_clone_form(req, ticket, data)
        action = self.ticket_clone_form_action or req.href.newticket()
        method = self.ticket_clone_form_method
        if method == "GET":
            field_name_fn = lambda name: name
        else:
            field_name_fn = lambda name: "field_%s" % name

        return tag.form(tag.div(tag.input(
            type="submit",
            name="clone",
            value=_("Clone"),
            title=_("Create a copy of this ticket")), [
                tag.input(type="hidden", name=field_name_fn(n), value=v)
                for n, v in fields.iteritems()
            ],
                                tag.input(type="hidden",
                                          name='preview',
                                          value=''),
                                class_="inlinebuttons"),
                        method=method,
                        action=action)
Esempio n. 3
0
class dbAuth(Component):
    """Data base based authorization module for Trac - to be used with account manager plugin"""
    
    implements(IPasswordStore)
    hash_method = ExtensionOption('account-manager', 'hash_method',
	    IPasswordHashMethod, 'HtDigestHashMethod')

    def __init__(self):
	"""Acquire db connection"""
	self._db = ScalakDB(self.compmgr)

    def get_users(self):
        """Returns an iterable of the known usernames"""

	result = self._db.execute(self._db.sql_get_users, (self._db.project_id,))
		       
	for user, in result:
	    yield user

 
    def has_user(self, user):
	result = self._db.execute(self._db.sql_has_user, (self._db.project_id, user))
	for row in result:
	    self.__close()
	    return True
	return False
 
    def set_password(self, user, password):
        """Changing password should be done by Scalak-admin or dashboard
        """
        raise TracError("Changing password should be done via dashboard, " \
                "new accounts should be created by Scalak administartor.")



    def check_password(self, user, password):
        """Checks if the password is valid for the user.
        """

	result = self._db.execute(self._db.sql_get_pass, 
                (user, self._db.project_id))

	for hash, in result:
	    return self.hash_method.check_hash(user, password, hash)

	return None

    def delete_user(self, user):
        """Deletes user from project
        Returns True if the account existed and was deleted, False otherwise.
        """

	if not self.has_user(user):
	    return False

	result = self._db.execute(self._db.sql_del_user, (user, self._db.project_id))
	return True
Esempio n. 4
0
class BaseIndexer(Component):
    """
    This is base class for Bloodhound Search indexers of specific resource
    """
    silence_on_error = BoolOption('bhsearch', 'silence_on_error', "True",
        """If true, do not throw an exception during indexing a resource""")

    wiki_formatter = ExtensionOption('bhsearch', 'wiki_syntax_formatter',
        ISearchWikiSyntaxFormatter, 'SimpleSearchWikiSyntaxFormatter',
        'Name of the component implementing wiki syntax to text formatter \
        interface: ISearchWikiSyntaxFormatter.')
Esempio n. 5
0
class RevtreeSystem(Component):
    """ """

    enhancers = ExtensionPoint(IRevtreeEnhancer)
    optimizer = ExtensionOption(
        'revtree', 'optimizer', IRevtreeOptimizer, 'DefaultRevtreeOptimizer',
        """Name of the component implementing `IRevtreeOptimizer`, which is 
        used for optimizing revtree element placements.""")

    def get_revtree(self, repos):
        self.urlbase = self.config.get('trac', 'base_url')
        if not self.urlbase:
            raise TracError, "Base URL not defined"
        self.env.log.debug("Enhancers: %s" % self.enhancers)
        from revtree.svgview import SvgRevtree
        return SvgRevtree(self.env, repos, self.urlbase, self.enhancers,
                          self.optimizer)
Esempio n. 6
0
class UserManager(Component):
    """The central place one goes to get a list of all users in the system."""
    
    implements(IRoleManager)

    implementation = ExtensionOption('trac', 'user_list', IRoleManager,
                            'DefaultRoleManager',
        """Name of the component implementing `IRoleManager`, which is used
        to collect the list of known users.""")

    def get_all_users(self):
        return self.implementation.get_all_users()
    
    def get_all_roles(self):
        return self.implementation.get_all_roles()
    
    def get_all_groups(self):
        return self.implementation.get_all_groups()
Esempio n. 7
0
class RevtreeSystem(Component):
    """Revision tree constructor"""

    enhancers = ExtensionPoint(IRevtreeEnhancer)
    optimizer = ExtensionOption(
        'revtree', 'optimizer', IRevtreeOptimizer, 'DefaultRevtreeOptimizer',
        """Name of the component implementing `IRevtreeOptimizer`, which is 
        used for optimizing revtree element placements.""")

    def get_revtree(self, repos, req):
        # ideally, the repository type should be requested from the repos
        # instance; however it is usually hidden behind the repository cache
        # that does not report the actual repository backend
        if self.config.get('trac', 'repository_type') != 'svn':
            raise TracError, "Revtree only supports Subversion repositories"
        self.env.log.debug("Enhancers: %s" % self.enhancers)
        from revtree.svgview import SvgRevtree
        return SvgRevtree(self.env, repos, req.href(), self.enhancers,
                          self.optimizer)
Esempio n. 8
0
class SuperUserPlugin(Component):
    """ Adds a superuser with TRAC_ADMIN permissions """
    implements(IPermissionStore)

    superusers = ListOption('superuser',
                            'superuser',
                            'admin',
                            doc='Superuser username(s) (comma separated)')

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

    def get_user_permissions(self, username):
        if username in self.superusers:
            return list(
                set(['TRAC_ADMIN'])
                | set(self.store.get_user_permissions(username)))
        else:
            return self.store.get_user_permissions(username)

    def get_users_with_permissions(self, permissions):
        return list(
            set(self.superusers)
            | set(self.store.get_users_with_permissions(permissions)))

    def get_all_permissions(self):
        return self.store.get_all_permissions() + [(user, 'TRAC_ADMIN')
                                                   for user in self.superusers]

    def grant_permission(self, username, action):
        if username in self.superusers:
            assert False, "Superuser %s can't be given any permissions" % username
        return self.store.grant_permission(username, action)

    def revoke_permission(self, username, action):
        if username in self.superusers:
            assert False, "Superuser %s can't be revoked any permissions" % username
        return self.store.revoke_permission(username, action)
Esempio n. 9
0
class MilestoneModule(Component):
    """View and edit individual milestones."""

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
               ITimelineEventProvider, IWikiSyntaxProvider, IResourceManager,
               ISearchSource)

    stats_provider = ExtensionOption(
        'milestone', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`, 
        which is used to collect statistics on groups of tickets for display
        in the milestone views.""")

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'roadmap'

    def get_navigation_items(self, req):
        return []

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = [
            'MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY',
            'MILESTONE_VIEW'
        ]
        return actions + [('MILESTONE_ADMIN', actions)]

    # ITimelineEventProvider methods

    def get_timeline_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _('Milestones reached'))

    def get_timeline_events(self, req, start, stop, filters):
        if 'milestone' in filters:
            milestone_realm = Resource('milestone')
            for completed, name, description in self.env.db_query(
                    """
                    SELECT completed, name, description FROM milestone
                    WHERE completed>=%s AND completed<=%s
                    """, (to_utimestamp(start), to_utimestamp(stop))):
                # TODO: creation and (later) modifications should also be
                #       reported
                milestone = milestone_realm(id=name)
                if 'MILESTONE_VIEW' in req.perm(milestone):
                    yield ('milestone', from_utimestamp(completed), '',
                           (milestone, description))  # FIXME: author?

            # Attachments
            for event in AttachmentModule(self.env).get_timeline_events(
                    req, milestone_realm, start, stop):
                yield event

    def render_timeline_event(self, context, field, event):
        milestone, description = event[3]
        if field == 'url':
            return context.href.milestone(milestone.id)
        elif field == 'title':
            return tag_('Milestone %(name)s completed',
                        name=tag.em(milestone.id))
        elif field == 'description':
            return format_to(self.env, None, context.child(resource=milestone),
                             description)

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/milestone(?:/(.+))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['id'] = match.group(1)
            return True

    def process_request(self, req):
        milestone_id = req.args.get('id')
        req.perm('milestone', milestone_id).require('MILESTONE_VIEW')

        add_link(req, 'up', req.href.roadmap(), _('Roadmap'))

        action = req.args.get('action', 'view')
        try:
            milestone = Milestone(self.env, milestone_id)
        except ResourceNotFound:
            if 'MILESTONE_CREATE' not in req.perm('milestone', milestone_id):
                raise
            milestone = Milestone(self.env, None)
            milestone.name = milestone_id
            action = 'edit'  # rather than 'new' so that it works for POST/save

        if req.method == 'POST':
            if req.args.has_key('cancel'):
                if milestone.exists:
                    req.redirect(req.href.milestone(milestone.name))
                else:
                    req.redirect(req.href.roadmap())
            elif action == 'edit':
                return self._do_save(req, milestone)
            elif action == 'delete':
                self._do_delete(req, milestone)
        elif action in ('new', 'edit'):
            return self._render_editor(req, milestone)
        elif action == 'delete':
            return self._render_confirm(req, milestone)

        if not milestone.name:
            req.redirect(req.href.roadmap())

        return self._render_view(req, milestone)

    # Internal methods

    def _do_delete(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        retarget_to = None
        if req.args.has_key('retarget'):
            retarget_to = req.args.get('target') or None
        milestone.delete(retarget_to, req.authname)
        add_notice(
            req,
            _('The milestone "%(name)s" has been deleted.',
              name=milestone.name))
        req.redirect(req.href.roadmap())

    def _do_save(self, req, milestone):
        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')

        old_name = milestone.name
        new_name = req.args.get('name')

        milestone.description = req.args.get('description', '')

        if 'due' in req.args:
            due = req.args.get('duedate', '')
            milestone.due = user_time(req, parse_date, due, hint='datetime') \
                            if due else None
        else:
            milestone.due = None

        completed = req.args.get('completeddate', '')
        retarget_to = req.args.get('target')

        # Instead of raising one single error, check all the constraints and
        # let the user fix them by going back to edit mode showing the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        # -- check the name
        # If the name has changed, check that the milestone doesn't already
        # exist
        # FIXME: the whole .exists business needs to be clarified
        #        (#4130) and should behave like a WikiPage does in
        #        this respect.
        try:
            new_milestone = Milestone(self.env, new_name)
            if new_milestone.name == old_name:
                pass  # Creation or no name change
            elif new_milestone.name:
                warn(
                    _(
                        'Milestone "%(name)s" already exists, please '
                        'choose another name.',
                        name=new_milestone.name))
            else:
                warn(_('You must provide a name for the milestone.'))
        except ResourceNotFound:
            milestone.name = new_name

        # -- check completed date
        if 'completed' in req.args:
            completed = user_time(req, parse_date, completed,
                                  hint='datetime') if completed else None
            if completed and completed > datetime.now(utc):
                warn(_('Completion date may not be in the future'))
        else:
            completed = None
        milestone.completed = completed

        if warnings:
            return self._render_editor(req, milestone)

        # -- actually save changes
        if milestone.exists:
            milestone.update()
            # eventually retarget opened tickets associated with the milestone
            if 'retarget' in req.args and completed:
                self.env.db_transaction(
                    """
                    UPDATE ticket SET milestone=%s
                    WHERE milestone=%s and status != 'closed'
                    """, (retarget_to, old_name))
                self.log.info("Tickets associated with milestone %s "
                              "retargeted to %s" % (old_name, retarget_to))
        else:
            milestone.insert()

        add_notice(req, _("Your changes have been saved."))
        req.redirect(req.href.milestone(milestone.name))

    def _render_confirm(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        milestones = [
            m for m in Milestone.select(self.env) if m.name != milestone.name
            and 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        data = {
            'milestone':
            milestone,
            'milestone_groups':
            group_milestones(milestones, 'TICKET_ADMIN' in req.perm)
        }
        return 'milestone_delete.html', data, None

    def _render_editor(self, req, milestone):
        # Suggest a default due time of 18:00 in the user's timezone
        default_due = datetime.now(req.tz).replace(hour=18,
                                                   minute=0,
                                                   second=0,
                                                   microsecond=0)
        if default_due <= datetime.now(utc):
            default_due += timedelta(days=1)

        data = {
            'milestone': milestone,
            'datetime_hint': get_datetime_format_hint(req.lc_time),
            'default_due': default_due,
            'milestone_groups': [],
        }

        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
            milestones = [
                m for m in Milestone.select(self.env)
                if m.name != milestone.name
                and 'MILESTONE_VIEW' in req.perm(m.resource)
            ]
            data['milestone_groups'] = group_milestones(
                milestones, 'TICKET_ADMIN' in req.perm)
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')

        chrome = Chrome(self.env)
        chrome.add_jquery_ui(req)
        chrome.add_wiki_toolbars(req)
        return 'milestone_edit.html', data, None

    def _render_view(self, req, milestone):
        milestone_groups = []
        available_groups = []
        component_group_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

        # collect fields that can be used for grouping
        for field in ticket_fields:
            if field['type'] == 'select' and field['name'] != 'milestone' \
                    or field['name'] in ('owner', 'reporter'):
                available_groups.append({
                    'name': field['name'],
                    'label': field['label']
                })
                if field['name'] == 'component':
                    component_group_available = True

        # determine the field currently used for grouping
        by = None
        if component_group_available:
            by = 'component'
        elif available_groups:
            by = available_groups[0]['name']
        by = req.args.get('by', by)

        tickets = get_tickets_for_milestone(self.env,
                                            milestone=milestone.name,
                                            field=by)
        tickets = apply_ticket_permissions(self.env, req, tickets)
        stat = get_ticket_stats(self.stats_provider, tickets)

        context = web_context(req, milestone.resource)
        data = {
            'context': context,
            'milestone': milestone,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'available_groups': available_groups,
            'grouped_by': by,
            'groups': milestone_groups
        }
        data.update(milestone_stats_data(self.env, req, stat, milestone.name))

        if by:

            def per_group_stats_data(gstat, group_name):
                return milestone_stats_data(self.env, req, gstat,
                                            milestone.name, by, group_name)

            milestone_groups.extend(
                grouped_stats_data(self.env, self.stats_provider, tickets, by,
                                   per_group_stats_data))

        add_stylesheet(req, 'common/css/roadmap.css')
        add_script(req, 'common/js/folding.js')

        def add_milestone_link(rel, milestone):
            href = req.href.milestone(milestone.name, by=req.args.get('by'))
            add_link(req, rel, href,
                     _('Milestone "%(name)s"', name=milestone.name))

        milestones = [
            m for m in Milestone.select(self.env)
            if 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        idx = [i for i, m in enumerate(milestones) if m.name == milestone.name]
        if idx:
            idx = idx[0]
            if idx > 0:
                add_milestone_link('first', milestones[0])
                add_milestone_link('prev', milestones[idx - 1])
            if idx < len(milestones) - 1:
                add_milestone_link('next', milestones[idx + 1])
                add_milestone_link('last', milestones[-1])
        prevnext_nav(req, _('Previous Milestone'), _('Next Milestone'),
                     _('Back to Roadmap'))

        return 'milestone_view.html', data, None

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('milestone', self._format_link)

    def _format_link(self, formatter, ns, name, label):
        name, query, fragment = formatter.split_link(name)
        return self._render_link(formatter.context, name, label,
                                 query + fragment)

    def _render_link(self, context, name, label, extra=''):
        try:
            milestone = Milestone(self.env, name)
        except TracError:
            milestone = None
        # Note: the above should really not be needed, `Milestone.exists`
        # should simply be false if the milestone doesn't exist in the db
        # (related to #4130)
        href = context.href.milestone(name)
        if milestone and milestone.exists:
            if 'MILESTONE_VIEW' in context.perm(milestone.resource):
                closed = 'closed ' if milestone.is_completed else ''
                return tag.a(label,
                             class_='%smilestone' % closed,
                             href=href + extra)
        elif 'MILESTONE_CREATE' in context.perm('milestone', name):
            return tag.a(label,
                         class_='missing milestone',
                         href=href + extra,
                         rel='nofollow')
        return tag.a(label, class_='missing milestone')

    # IResourceManager methods

    def get_resource_realms(self):
        yield 'milestone'

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        desc = resource.id
        if format != 'compact':
            desc = _('Milestone %(name)s', name=resource.id)
        if context:
            return self._render_link(context, resource.id, desc)
        else:
            return desc

    def resource_exists(self, resource):
        """
        >>> from trac.test import EnvironmentStub
        >>> env = EnvironmentStub()
        
        >>> m1 = Milestone(env)
        >>> m1.name = 'M1'
        >>> m1.insert()
        
        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M1'))
        True
        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M2'))
        False
        """
        return bool(
            self.env.db_query(
                """
                SELECT name FROM milestone WHERE name=%s""", (resource.id, )))

    # ISearchSource methods

    def get_search_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _('Milestones'))

    def get_search_results(self, req, terms, filters):
        if not 'milestone' in filters:
            return
        with self.env.db_query as db:
            sql_query, args = search_to_sql(db, ['name', 'description'], terms)

            milestone_realm = Resource('milestone')
            for name, due, completed, description in db(
                    """
                    SELECT name, due, completed, description FROM milestone
                    WHERE """ + sql_query, args):
                milestone = milestone_realm(id=name)
                if 'MILESTONE_VIEW' in req.perm(milestone):
                    dt = (from_utimestamp(completed) if completed else
                          from_utimestamp(due) if due else datetime.now(utc))
                    yield (get_resource_url(self.env, milestone, req.href),
                           get_resource_name(self.env, milestone), dt, '',
                           shorten_result(description, terms))

        # Attachments
        for result in AttachmentModule(self.env).get_search_results(
                req, milestone_realm, terms):
            yield result
Esempio n. 10
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)]
Esempio n. 11
0
class NotificationSystem(Component):

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

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

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

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

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

    smtp_from_author = BoolOption('notification', 'smtp_from_author', 'false',
        """Use the action author as the sender of notification emails.
           (''since 1.0'')""")

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

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

    smtp_always_bcc_list = ListOption(
        'notification', 'smtp_always_bcc', '', sep=(',', ' '),
        doc="""Comma-separated list of email address(es) to always send
        notifications to, addresses do not appear publicly (Bcc:).
        (''since 0.10'')""")

    smtp_default_domain = Option('notification', 'smtp_default_domain', '',
        """Default host/domain to append to address that do not specify
           one.""")

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

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

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

        Valid options are 'base64' for Base64 encoding, 'qp' for
        Quoted-Printable, and 'none' for no encoding, in which case mails will
        be sent as 7bit if the content is all ASCII, or 8bit otherwise.
        (''since 0.10'')""")

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

        If this option is disabled, recipients are put on BCC.
        (''since 0.10'')""")

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

        The SMTP server should accept those addresses, and either append
        a FQDN or use local delivery. (''since 0.10'')""")

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

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

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

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

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

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

    def send_email(self, from_addr, recipients, message):
        """Send message to recipients via e-mail."""
        self.email_sender.send(from_addr, recipients, message)
Esempio n. 12
0
class ProgressMeterMacro(WikiMacroBase):
    """Progress meter wiki macro plugin for Trac

    Usage instructions are available at:
        http://trac-hacks.org/wiki/ProgressMeterMacro
    """
    implements(ITemplateProvider)

    _sp = ExtensionOption(
        'progressmeter', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
        which is used to collect statistics on groups of tickets
        for meters generated by the ProgressMeterMacro plugin.""")

    _ticket_re = re.compile(r'/ticket/([0-9]+)$')

    def _this_ticket(self, req):
        match = self._ticket_re.match(req.path_info)
        if match:
            return match.group(1)
        else:
            assert req.path_info == '/newticket', "The `self` " \
              "keyword is permitted in ticket descriptions only."
            return None

    def _parse_macro_content(self, content, req):
        args, kwargs = parse_args(content, strict=False)
        kwargs['max'] = 0
        kwargs['order'] = 'id'
        kwargs['col'] = 'id'

        # special case for values equal to 'self': replace with current ticket
        # number, if available
        preview = False
        for key in kwargs.keys():
            if kwargs[key] == 'self':
                current_ticket = self._this_ticket(req)
                if current_ticket:
                    kwargs[key] = current_ticket
                else:
                    # id=0 basically causes a dummy preview of the meter
                    # to be rendered
                    preview = True
                    kwargs = {'id': 0}
                    break

        try:
            spkw = kwargs.pop('stats_provider')
            xtnpt = ExtensionPoint(ITicketGroupStatsProvider)

            found = False
            for impl in xtnpt.extensions(self):
                if impl.__class__.__name__ == spkw:
                    found = True
                    stats_provider = impl
                    break

            if not found:
                raise TracError("Supplied stats provider does not exist!")
        except KeyError:
            # if the `stats_provider` keyword argument is not provided,
            # propagate the stats provider defined in the config file
            stats_provider = self._sp

        return stats_provider, kwargs, preview

    def expand_macro(self, formatter, name, content):
        req = formatter.req
        stats_provider, kwargs, preview = self._parse_macro_content(
            content, req)

        # Create & execute the query string
        qstr = '&'.join(['%s=%s' % item for item in kwargs.iteritems()])
        query = Query.from_string(self.env, qstr)

        # Calculate stats
        qres = query.execute(req)
        tickets = apply_ticket_permissions(self.env, req, qres)

        stats = get_ticket_stats(stats_provider, tickets)
        stats_data = query_stats_data(req, stats, query.constraints)

        # ... and finally display them
        add_stylesheet(req, 'common/css/roadmap.css')
        chrome = Chrome(self.env)
        stats_data.update({'preview': preview})  # displaying a preview?
        return chrome.render_template(req,
                                      'progressmeter.html',
                                      stats_data,
                                      fragment=True)

    ## ITemplateProvider methods
    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]
Esempio n. 13
0
class PermissionSystem(Component):
    """Sub-system that manages user permissions."""

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

    # 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, u"%s n'est pas une action valide" % 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(self):
        actions = []
        for requestor in self.requestors:
            for action in requestor.get_permission_actions():
                if isinstance(action, tuple):
                    actions.append(action[0])
                else:
                    actions.append(action)
        return 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 as keys, and
        a boolean value. `True` means that the permission is granted, `False`
        means the permission is denied."""
        actions = []
        for requestor in self.requestors:
            actions += list(requestor.get_permission_actions())
        permissions = {}
        if username:
            # Return all permissions that the given user has
            meta = {}
            for action in actions:
                if isinstance(action, tuple):
                    name, value = action
                    meta[name] = value

            def _expand_meta(action):
                permissions[action] = True
                if meta.has_key(action):
                    [_expand_meta(perm) for perm in meta[action]]

            for perm in self.store.get_user_permissions(username):
                _expand_meta(perm)
        else:
            # Return all permissions available in the system
            for action in actions:
                if isinstance(action, tuple):
                    permissions[action[0]] = True
                else:
                    permissions[action] = True
        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()

    # IPermissionRequestor methods

    def get_permission_actions(self):
        """Implement the global `TRAC_ADMIN` meta permission."""
        actions = []
        for requestor in [r for r in self.requestors if r is not self]:
            for action in requestor.get_permission_actions():
                if isinstance(action, tuple):
                    actions.append(action[0])
                else:
                    actions.append(action)
        return [('TRAC_ADMIN', actions)]
Esempio n. 14
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
Esempio n. 15
0
class VisibleVersion(Component):
    implements(ILegacyAttachmentPolicyDelegate, INavigationContributor,
               IPermissionRequestor, IRequestHandler, IResourceManager,
               ITemplateProvider, IWikiSyntaxProvider)

    navigation_item = Option(
        'extended_version', 'navigation_item', 'roadmap',
        """The main navigation item to highlight when displaying versions.""")

    show_milestone_description = BoolOption(
        'extended_version', 'show_milestone_description', False,
        """whether to display milestone descriptions on version page.""")

    version_stats_provider = ExtensionOption(
        'extended_version', 'version_stats_provider',
        ITicketGroupStatsProvider, 'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
           which is used to collect statistics on all version tickets.""")

    milestone_stats_provider = ExtensionOption(
        'extended_version', 'milestone_stats_provider',
        ITicketGroupStatsProvider, 'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
           which is used to collect statistics on per milestone tickets in
           the version view.""")

    # ILegacyAttachmentPolicyDelegate methods

    def check_attachment_permission(self, action, username, resource, perm):
        if resource.parent.realm != 'version':
            return

        if action == 'ATTACHMENT_CREATE':
            action = 'VERSION_MODIFY'
        elif action == 'ATTACHMENT_VIEW':
            action = 'VERSION_VIEW'
        elif action == 'ATTACHMENT_DELETE':
            action = 'VERSION_DELETE'

        decision = action in perm(resource.parent)
        if not decision:
            self.env.log.debug('ExtendedVersionPlugin denied %s '
                               'access to %s. User needs %s' %
                               (username, resource, action))
        return decision

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'versions'

    def get_navigation_items(self, req):
        return []

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = [
            'VERSION_CREATE', 'VERSION_DELETE', 'VERSION_MODIFY',
            'VERSION_VIEW'
        ]
        return actions + [('VERSION_ADMIN', actions)]

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/version(?:/(.*))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['id'] = match.group(1)
            return True

    def process_request(self, req):
        version_id = req.args.get('id')
        req.perm('version', version_id).require('VERSION_VIEW')

        version = Version(self.env, version_id)
        action = req.args.get('action', 'view')

        if req.method == 'POST':
            if 'cancel' in req.args:
                if version.exists:
                    req.redirect(req.href.version(version.name))
                else:
                    req.redirect(req.href.versions())
            elif action == 'edit':
                return self._do_save(req, version)
            elif action == 'delete':
                self._do_delete(req, version)
        elif action in ('new', 'edit'):
            return self._render_editor(req, version)
        elif action == 'delete':
            return self._render_confirm(req, version)

        if not version.name:
            req.redirect(req.href.versions())

        add_stylesheet(req, 'common/css/roadmap.css')
        return self._render_view(req, version)

    # IResourceManager methods

    # TODO: not sure this is implemented right just yet,
    # and do we need to implement get_resource_url?

    def get_resource_realms(self):
        yield 'version'

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        desc = resource.id
        if format != 'compact':
            desc = _('Version %(name)s', name=resource.id)
        if context:
            return tag.a('Version %(name)s',
                         name=resource.id,
                         href=context.href.version(resource.id))
        else:
            return desc

    def resource_exists(self, resource):
        try:
            Version(self.env, resource.id)
            return Version.exists
        except ResourceNotFound:
            return False

    # ITemplateProvider methods

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

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

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('version', self._format_link)

    # Internal methods

    def _do_delete(self, req, version):
        name = version.name
        resource = Resource('version', name)
        req.perm(resource).require('VERSION_DELETE')

        with self.env.db_transaction as db:
            db(
                """
                DELETE FROM milestone_version WHERE version=%s
                """, (name, ))
            version.delete()
        add_notice(req, _('The version "%(name)s" has been deleted.',
                          name=name))
        req.redirect(req.href.versions())

    def _do_save(self, req, version):
        resource = Resource('version', version.name)
        if version.exists:
            req.perm(resource).require('VERSION_MODIFY')
        else:
            req.perm(resource).require('VERSION_CREATE')

        old_name = version.name
        new_name = req.args.get('name')

        version.name = new_name
        version.description = req.args.get('description', '')

        time = req.args.get('time', '')

        # Instead of raising one single error, check all the constraints and
        # let the user fix them by going back to edit mode showing the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        # -- check the name
        if new_name:
            if new_name != old_name:
                # check that the version doesn't already exists
                # FIXME: the whole .exists business needs to be clarified
                #        (#4130) and should behave like a WikiPage does in
                #        this respect.
                try:
                    Version(self.env, new_name)
                    warn(
                        _(
                            'Version "%(name)s" already exists, please '
                            'choose another name',
                            name=new_name))
                except ResourceNotFound:
                    pass
        else:
            warn(_('You must provide a name for the version.'))

        # -- check completed date
        if 'released' in req.args:
            time = user_time(req, parse_date, time, hint='datetime') \
                   if time else None
            if time and time > datetime.now(utc):
                warn(_("Release date may not be in the future"))
        else:
            time = None
        version.time = time

        if warnings:
            return self._render_editor(req, version)

        # -- actually save changes
        with self.env.db_transaction as db:
            if version.exists:
                if version.name != version._old_name:
                    # Update tickets
                    db(
                        """
                        UPDATE milestone_version SET version=%s WHERE version=%s
                        """, (version.name, version._old_name))
                version.update()
            else:
                version.insert()

        req.redirect(req.href.version(version.name))

    def _format_link(self, formatter, ns, name, label):
        name, query, fragment = formatter.split_link(name)
        return self._render_link(formatter.context, name, label,
                                 query + fragment)

    def _render_confirm(self, req, version):
        resource = Resource('version', version.name)
        req.perm(resource).require('VERSION_DELETE')

        data = {
            'version': version,
        }

        add_stylesheet(req, 'common/css/roadmap.css')
        return 'version_delete.html', data, None

    def _render_editor(self, req, version):
        resource = Resource('version', version.name)
        data = {
            'version': version,
            'resource': resource,
            'versions': [ver.name for ver in Version.select(self.env)],
            'datetime_hint': get_datetime_format_hint(),
            'version_groups': [],
        }

        if version.exists:
            req.perm(resource).require('VERSION_MODIFY')
            #versions = [m for m in Version.select(self.env)
            #              if m.name != version.name
            #              and 'VERSION_VIEW' in req.perm(m.resource)]
        else:
            req.perm(resource).require('VERSION_CREATE')

        Chrome(self.env).add_jquery_ui(req)
        Chrome(self.env).add_wiki_toolbars(req)
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'version_edit.html', data, None

    def _render_link(self, context, name, label, extra=''):
        try:
            version = Version(self.env, name)
        except TracError:
            version = None
        # Note: the above should really not be needed, `Milestone.exists`
        # should simply be false if the milestone doesn't exist in the db
        # (related to #4130)
        href = context.href.version(name)
        if version and version.exists:
            resource = Resource('version', name)
            if 'VERSION_VIEW' in context.perm(resource):
                return tag.a(label, class_='version', href=href + extra)
        elif 'VERSION_CREATE' in context.perm('version', name):
            return tag.a(label,
                         class_='missing version',
                         href=href + extra,
                         rel='nofollow')
        return tag.a(label, class_='missing version')

    def _render_view(self, req, version):
        milestones = []
        tickets = []
        milestone_stats = []

        for name, in self.env.db_query(
                """
                SELECT name FROM milestone
                 INNER JOIN milestone_version ON (name = milestone)
                WHERE version = %s
                ORDER BY due
                """, (version.name, )):
            milestone = Milestone(self.env, name)
            milestones.append(milestone)

            mtickets = get_tickets_for_milestone(self.env, milestone.name,
                                                 'owner')
            mtickets = apply_ticket_permissions(self.env, req, mtickets)
            tickets += mtickets
            stat = get_ticket_stats(self.milestone_stats_provider, mtickets)
            milestone_stats.append(
                milestone_stats_data(self.env, req, stat, milestone.name))

        stats = get_ticket_stats(self.version_stats_provider, tickets)
        interval_hrefs = version_interval_hrefs(
            self.env, req, stats, [milestone.name for milestone in milestones])

        version.resource = Resource('version', version.name)
        context = web_context(req, version.resource)

        version.is_released = version.time \
            and version.time < datetime.now(utc)
        version.stats = stats
        version.interval_hrefs = interval_hrefs
        names = [milestone.name for milestone in milestones]
        version.stats_href = version_stats_href(self.env, req, names)
        data = {
            'version': version,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'milestones': milestones,
            'milestone_stats': milestone_stats,
            'show_milestone_description':
            self.show_milestone_description  # Not implemented yet
        }

        add_stylesheet(req, 'extendedversion/css/version.css')
        add_script(req, 'common/js/folding.js')
        add_ctxtnav(req, _("Back to Versions"), req.href.versions())
        return 'version_view.html', data, None
Esempio n. 16
0
class SessionStore(Component):
    implements(IPasswordStore)

    hash_method = ExtensionOption('account-manager', 'hash_method',
        IPasswordHashMethod, 'HtDigestHashMethod',
        doc = N_("IPasswordHashMethod used to create new/updated passwords"))

    def __init__(self):
        self.key = 'password'
        # Check for valid hash method configuration.
        self.hash_method_enabled

    def get_users(self):
        """Returns an iterable of the known usernames."""
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT DISTINCT sid
            FROM    session_attribute
            WHERE   authenticated=1
                AND name=%s
            """, (self.key,))
        for sid, in cursor:
            yield sid
 
    def has_user(self, user):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT  *
            FROM    session_attribute
            WHERE   authenticated=1
                AND name=%s
                AND sid=%s
            """, (self.key, user))
        for row in cursor:
            return True
        return False

    def set_password(self, user, password, old_password=None):
        """Sets the password for the user.

        This should create the user account, if it doesn't already exist.
        Returns True, if a new account was created, and False,
        if an existing account was updated.
        """
        if not self.hash_method_enabled:
            return
        hash = self.hash_method.generate_hash(user, password)
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        sql = """
            WHERE   authenticated=1
                AND name=%s
                AND sid=%s
            """
        cursor.execute("""
            UPDATE  session_attribute
                SET value=%s
            """ + sql, (hash, self.key, user))
        cursor.execute("""
            SELECT  value
            FROM    session_attribute
            """ + sql, (self.key, user))
        not_exists = cursor.fetchone() is None
        if not_exists:
            cursor.execute("""
                INSERT INTO session_attribute
                        (sid,authenticated,name,value)
                VALUES  (%s,1,%s,%s)
                """, (user, self.key, hash))
        db.commit()
        return not_exists

    def check_password(self, user, password):
        """Checks if the password is valid for the user."""
        if not self.hash_method_enabled:
            return
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT  value
            FROM    session_attribute
            WHERE   authenticated=1
                AND name=%s
                AND sid=%s
            """, (self.key, user))
        for hash, in cursor:
            return self.hash_method.check_hash(user, password, hash)
        # Return value 'None' allows to proceed with another, chained store.
        return

    def delete_user(self, user):
        """Deletes the user account.

        Returns True, if the account existed and was deleted, False otherwise.
        """
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        sql = """
            WHERE   authenticated=1
                AND name=%s
                AND sid=%s
            """
        # Avoid has_user() to make this transaction atomic.
        cursor.execute("""
            SELECT  *
            FROM    session_attribute
            """ + sql, (self.key, user))
        exists = cursor.fetchone() is not None
        if exists:
            cursor.execute("""
                DELETE
                FROM    session_attribute
                """ + sql, (self.key, user))
            db.commit()
        return exists

    @property
    def hash_method_enabled(self):
        try:
            hash_method = self.hash_method
        except AttributeError:
            self.env.log.error("%s: no IPasswordHashMethod enabled "
                               "- fatal, can't work" % self.__class__)
            return
        return True
Esempio n. 17
0
class MilestoneModule(Component):
    """View and edit individual milestones."""

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
               IResourceManager, ISearchSource, ITimelineEventProvider,
               IWikiSyntaxProvider)

    realm = 'milestone'

    stats_provider = ExtensionOption(
        'milestone', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
        which is used to collect statistics on groups of tickets for display
        in the milestone views.""")

    default_retarget_to = Option(
        'milestone',
        'default_retarget_to',
        doc="""Default milestone to which tickets are retargeted when
            closing or deleting a milestone. (''since 1.1.2'')""")

    default_group_by = Option(
        'milestone', 'default_group_by', 'component',
        """Default field to use for grouping tickets in the grouped
        progress bar. (''since 1.2'')""")

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'roadmap'

    def get_navigation_items(self, req):
        return []

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = [
            'MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY',
            'MILESTONE_VIEW'
        ]
        return actions + [('MILESTONE_ADMIN', actions)]

    # ITimelineEventProvider methods

    def get_timeline_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _("Milestones completed"))

    def get_timeline_events(self, req, start, stop, filters):
        if 'milestone' in filters:
            milestone_realm = Resource(self.realm)
            for name, due, completed, description \
                    in MilestoneCache(self.env).milestones.itervalues():
                if completed and start <= completed <= stop:
                    # TODO: creation and (later) modifications should also be
                    #       reported
                    milestone = milestone_realm(id=name)
                    if 'MILESTONE_VIEW' in req.perm(milestone):
                        yield (
                            'milestone',
                            completed,
                            '',  # FIXME: author?
                            (milestone, description))

            # Attachments
            for event in AttachmentModule(self.env).get_timeline_events(
                    req, milestone_realm, start, stop):
                yield event

    def render_timeline_event(self, context, field, event):
        milestone, description = event[3]
        if field == 'url':
            return context.href.milestone(milestone.id)
        elif field == 'title':
            return tag_("Milestone %(name)s completed",
                        name=tag.em(milestone.id))
        elif field == 'description':
            child_resource = context.child(resource=milestone)
            return format_to(self.env, None, child_resource, description)

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/milestone(?:/(.+))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['id'] = match.group(1)
            return True

    def process_request(self, req):
        milestone_id = req.args.get('id')
        action = req.args.get('action', 'view')
        if not milestone_id and action == 'view':
            req.redirect(req.href.roadmap())
        req.perm(self.realm, milestone_id).require('MILESTONE_VIEW')

        add_link(req, 'up', req.href.roadmap(), _("Roadmap"))

        try:
            milestone = Milestone(self.env, milestone_id)
        except ResourceNotFound:
            if 'MILESTONE_CREATE' not in req.perm(self.realm, milestone_id):
                raise
            milestone = Milestone(self.env)
            milestone.name = milestone_id
            action = 'edit'  # rather than 'new', so it works for POST/save

        if req.method == 'POST':
            if 'cancel' in req.args:
                if milestone.exists:
                    req.redirect(req.href.milestone(milestone.name))
                else:
                    req.redirect(req.href.roadmap())
            elif action == 'edit':
                return self._do_save(req, milestone)
            elif action == 'delete':
                self._do_delete(req, milestone)
            else:
                raise HTTPBadRequest(_("Invalid request arguments."))
        elif action in ('new', 'edit'):
            return self._render_editor(req, milestone)
        elif action == 'delete':
            return self._render_confirm(req, milestone)

        if not milestone.name:
            req.redirect(req.href.roadmap())

        return self._render_view(req, milestone)

    # Public methods

    def get_default_due(self, req):
        """Returns a `datetime` object representing the default due date in
        the user's timezone. The default due time is 18:00 in the user's
        time zone.
        """
        now = datetime_now(req.tz)
        default_due = datetime(now.year, now.month, now.day, 18)
        if now.hour > 18:
            default_due += timedelta(days=1)
        return to_datetime(default_due, req.tz)

    def save_milestone(self, req, milestone):
        # Instead of raising one single error, check all the constraints
        # and let the user fix them by going back to edit mode and showing
        # the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        milestone.description = req.args.get('description', '')

        if 'due' in req.args:
            duedate = req.args.get('duedate')
            milestone.due = user_time(req, parse_date, duedate,
                                      hint='datetime') \
                            if duedate else None
        else:
            milestone.due = None

        # -- check completed date
        if 'completed' in req.args:
            completed = req.args.get('completeddate', '')
            completed = user_time(req, parse_date, completed,
                                  hint='datetime') if completed else None
            if completed and completed > datetime_now(utc):
                warn(_("Completion date may not be in the future"))
        else:
            completed = None
        milestone.completed = completed

        # -- check the name
        # If the name has changed, check that the milestone doesn't already
        # exist
        # FIXME: the whole .exists business needs to be clarified
        #        (#4130) and should behave like a WikiPage does in
        #        this respect.
        new_name = req.args.get('name')
        try:
            new_milestone = Milestone(self.env, new_name)
        except ResourceNotFound:
            milestone.name = new_name
        else:
            if new_milestone.name != milestone.name:
                if new_milestone.name:
                    warn(
                        _(
                            'Milestone "%(name)s" already exists, please '
                            'choose another name.',
                            name=new_milestone.name))
                else:
                    warn(_("You must provide a name for the milestone."))

        if warnings:
            return False

        # -- actually save changes
        if milestone.exists:
            milestone.update(author=req.authname)
            if completed and 'retarget' in req.args:
                comment = req.args.get('comment', '')
                retarget_to = req.args.get('target') or None
                retargeted_tickets = \
                    milestone.move_tickets(retarget_to, req.authname,
                                           comment, exclude_closed=True)
                add_notice(
                    req,
                    _(
                        'The open tickets associated with '
                        'milestone "%(name)s" have been retargeted '
                        'to milestone "%(retarget)s".',
                        name=milestone.name,
                        retarget=retarget_to))
                new_values = {'milestone': retarget_to}
                comment = comment or \
                          _("Open tickets retargeted after milestone closed")
                event = BatchTicketChangeEvent(retargeted_tickets, None,
                                               req.authname, comment,
                                               new_values, None)
                try:
                    NotificationSystem(self.env).notify(event)
                except Exception as e:
                    self.log.error(
                        "Failure sending notification on ticket "
                        "batch change: %s", exception_to_unicode(e))
                    add_warning(
                        req,
                        tag_(
                            "The changes have been saved, but "
                            "an error occurred while sending "
                            "notifications: %(message)s",
                            message=to_unicode(e)))
            add_notice(req, _("Your changes have been saved."))
        else:
            milestone.insert()
            add_notice(
                req,
                _('The milestone "%(name)s" has been added.',
                  name=milestone.name))

        return True

    # Internal methods

    _default_retarget_to = default_retarget_to

    @property
    def default_retarget_to(self):
        if self._default_retarget_to and \
           not any(self._default_retarget_to == m.name
                   for m in Milestone.select(self.env)):
            self.log.warning(
                'Milestone "%s" does not exist. Update the '
                '"default_retarget_to" option in the '
                '[milestone] section of trac.ini', self._default_retarget_to)
        return self._default_retarget_to

    def _do_delete(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        retarget_to = req.args.get('target') or None
        # Don't translate ticket comment (comment:40:ticket:5658)
        retargeted_tickets = \
            milestone.move_tickets(retarget_to, req.authname,
                "Ticket retargeted after milestone deleted")
        milestone.delete()
        add_notice(
            req,
            _('The milestone "%(name)s" has been deleted.',
              name=milestone.name))
        if retargeted_tickets:
            add_notice(
                req,
                _(
                    'The tickets associated with milestone '
                    '"%(name)s" have been retargeted to milestone '
                    '"%(retarget)s".',
                    name=milestone.name,
                    retarget=retarget_to))
            new_values = {'milestone': retarget_to}
            comment = _("Tickets retargeted after milestone deleted")
            event = BatchTicketChangeEvent(retargeted_tickets, None,
                                           req.authname, comment, new_values,
                                           None)
            try:
                NotificationSystem(self.env).notify(event)
            except Exception as e:
                self.log.error(
                    "Failure sending notification on ticket batch "
                    "change: %s", exception_to_unicode(e))
                add_warning(
                    req,
                    tag_(
                        "The changes have been saved, but an "
                        "error occurred while sending "
                        "notifications: %(message)s",
                        message=to_unicode(e)))

        req.redirect(req.href.roadmap())

    def _do_save(self, req, milestone):
        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')

        if self.save_milestone(req, milestone):
            req.redirect(req.href.milestone(milestone.name))

        return self._render_editor(req, milestone)

    def _render_confirm(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        milestones = [
            m for m in Milestone.select(self.env) if m.name != milestone.name
            and 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        attachments = Attachment.select(self.env, self.realm, milestone.name)
        data = {
            'milestone':
            milestone,
            'milestone_groups':
            group_milestones(milestones, 'TICKET_ADMIN' in req.perm),
            'num_tickets':
            get_num_tickets_for_milestone(self.env, milestone),
            'retarget_to':
            self.default_retarget_to,
            'attachments':
            list(attachments)
        }
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_delete.html', data

    def _render_editor(self, req, milestone):
        data = {
            'milestone': milestone,
            'datetime_hint': get_datetime_format_hint(req.lc_time),
            'default_due': self.get_default_due(req),
            'milestone_groups': [],
        }

        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
            milestones = [
                m for m in Milestone.select(self.env)
                if m.name != milestone.name
                and 'MILESTONE_VIEW' in req.perm(m.resource)
            ]
            data['milestone_groups'] = \
                group_milestones(milestones, 'TICKET_ADMIN' in req.perm)
            data['num_open_tickets'] = \
                get_num_tickets_for_milestone(self.env, milestone,
                                              exclude_closed=True)
            data['retarget_to'] = self.default_retarget_to
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')
            if milestone.name:
                add_notice(
                    req,
                    _(
                        "Milestone %(name)s does not exist. You "
                        "can create it here.",
                        name=milestone.name))

        chrome = Chrome(self.env)
        chrome.add_jquery_ui(req)
        chrome.add_wiki_toolbars(req)
        chrome.add_auto_preview(req)
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_edit.html', data

    def _render_view(self, req, milestone):
        milestone_groups = []
        available_groups = []
        default_group_by_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

        # collect fields that can be used for grouping
        for field in ticket_fields:
            if field['type'] == 'select' and field['name'] != 'milestone' \
                    or field['name'] in ('owner', 'reporter'):
                available_groups.append({
                    'name': field['name'],
                    'label': field['label']
                })
                if field['name'] == self.default_group_by:
                    default_group_by_available = True

        # determine the field currently used for grouping
        by = None
        if default_group_by_available:
            by = self.default_group_by
        elif available_groups:
            by = available_groups[0]['name']
        by = req.args.get('by', by)

        tickets = get_tickets_for_milestone(self.env,
                                            milestone=milestone.name,
                                            field=by)
        tickets = apply_ticket_permissions(self.env, req, tickets)
        stat = get_ticket_stats(self.stats_provider, tickets)

        context = web_context(req, milestone.resource)
        data = {
            'context': context,
            'milestone': milestone,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'available_groups': available_groups,
            'grouped_by': by,
            'groups': milestone_groups
        }
        data.update(milestone_stats_data(self.env, req, stat, milestone.name))

        if by:

            def per_group_stats_data(gstat, group_name):
                return milestone_stats_data(self.env, req, gstat,
                                            milestone.name, by, group_name)

            milestone_groups.extend(
                grouped_stats_data(self.env, self.stats_provider, tickets, by,
                                   per_group_stats_data))

        add_stylesheet(req, 'common/css/roadmap.css')

        def add_milestone_link(rel, milestone):
            href = req.href.milestone(milestone.name, by=req.args.get('by'))
            add_link(req, rel, href,
                     _('Milestone "%(name)s"', name=milestone.name))

        milestones = [
            m for m in Milestone.select(self.env)
            if 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        idx = [i for i, m in enumerate(milestones) if m.name == milestone.name]
        if idx:
            idx = idx[0]
            if idx > 0:
                add_milestone_link('first', milestones[0])
                add_milestone_link('prev', milestones[idx - 1])
            if idx < len(milestones) - 1:
                add_milestone_link('next', milestones[idx + 1])
                add_milestone_link('last', milestones[-1])
        prevnext_nav(req, _("Previous Milestone"), _("Next Milestone"),
                     _("Back to Roadmap"))

        return 'milestone_view.html', data

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('milestone', self._format_link)

    def _format_link(self, formatter, ns, name, label):
        name, query, fragment = formatter.split_link(name)
        return self._render_link(formatter.context, name, label,
                                 query + fragment)

    def _render_link(self, context, name, label, extra=''):
        if not (name or extra):
            return tag()
        try:
            milestone = Milestone(self.env, name)
        except ResourceNotFound:
            milestone = None
        # Note: the above should really not be needed, `Milestone.exists`
        # should simply be false if the milestone doesn't exist in the db
        # (related to #4130)
        href = context.href.milestone(name)
        exists = milestone and milestone.exists
        if exists:
            if 'MILESTONE_VIEW' in context.perm(milestone.resource):
                title = None
                if hasattr(context, 'req'):
                    if milestone.is_completed:
                        title = _("Completed %(duration)s ago (%(date)s)",
                                  duration=pretty_timedelta(
                                      milestone.completed),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.completed))
                    elif milestone.is_late:
                        title = _("%(duration)s late (%(date)s)",
                                  duration=pretty_timedelta(milestone.due),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.due))
                    elif milestone.due:
                        title = _("Due in %(duration)s (%(date)s)",
                                  duration=pretty_timedelta(milestone.due),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.due))
                    else:
                        title = _("No date set")
                closed = 'closed ' if milestone.is_completed else ''
                return tag.a(label,
                             class_='%smilestone' % closed,
                             href=href + extra,
                             title=title)
        elif 'MILESTONE_CREATE' in context.perm(self.realm, name):
            return tag.a(label,
                         class_='missing milestone',
                         href=href + extra,
                         rel='nofollow')
        return tag.a(label, class_=classes('milestone', missing=not exists))

    # IResourceManager methods

    def get_resource_realms(self):
        yield self.realm

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        desc = resource.id
        if format != 'compact':
            desc = _("Milestone %(name)s", name=resource.id)
        if context:
            return self._render_link(context, resource.id, desc)
        else:
            return desc

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

        >>> m1 = Milestone(env)
        >>> m1.name = 'M1'
        >>> m1.insert()

        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M1'))
        True
        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M2'))
        False
        """
        return resource.id in MilestoneCache(self.env).milestones

    # ISearchSource methods

    def get_search_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _("Milestones"))

    def get_search_results(self, req, terms, filters):
        if 'milestone' not in filters:
            return
        term_regexps = search_to_regexps(terms)
        milestone_realm = Resource(self.realm)
        for name, due, completed, description \
                in MilestoneCache(self.env).milestones.itervalues():
            if all(
                    r.search(description) or r.search(name)
                    for r in term_regexps):
                milestone = milestone_realm(id=name)
                if 'MILESTONE_VIEW' in req.perm(milestone):
                    dt = (completed
                          if completed else due if due else datetime_now(utc))
                    yield (get_resource_url(self.env, milestone, req.href),
                           get_resource_name(self.env, milestone), dt, '',
                           shorten_result(description, terms))

        # Attachments
        for result in AttachmentModule(self.env).get_search_results(
                req, milestone_realm, terms):
            yield result
Esempio n. 18
0
class SharedPermsStore(Component):
    implements(IPermissionStore)
    implements(IPermissionRequestor)

    store = ExtensionOption(
        'sharedperms', 'wrapped_permission_store', IPermissionStore,
        'DefaultPermissionStore',
        """Name of the component implementing `IPermissionStore`, which is used
        for managing user and group permissions.""")
    global_prefix = Option('sharedperms', 'global_prefix', '#',
                           'Prefix for global users and groups')

    def get_user_permissions(self, username):

        #Inicialmente añadimos 'usuario' y '#usuario'
        subjects = set([username])

        local_permissions = []
        global_permissions = []

        #Recuperar filas locales
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("SELECT username,action FROM permission")
        rows = cursor.fetchall()
        for user, action in rows:
            local_permissions.append((user, action))

        env = get_master_env(self.env)
        if env != self.env:
            #Recuperar filas de proyecto maestro, sólo las que empiecen por global_prefix
            db = env.get_db_cnx()
            cursor = db.cursor()
            cursor.execute(
                "SELECT username,action FROM permission WHERE username LIKE %s",
                (self.global_prefix + "%", ))
            rows = cursor.fetchall()

            for user, action in rows:
                global_permissions.append((user, action))
        else:
            global_permissions = local_permissions

        for provider in self.store.group_providers:
            subjects.update(provider.get_permission_groups(username))

        #Permisos del usuario
        actions = set([])
        #Subjects (usuarios y grupos) que se han encontrado definidos localmente
        locally_processed = set([])
        #Subjects de los que se quiere heredar los permisos
        inherit_pending = set([])

        #Ahora proceder con expansión de permisos. Es un doble bucle. Primero
        #intentamos expansión local (lo que hacía el código original), y a
        #continuación se intenta la expansión en el proyecto maestro

        while True:

            while True:

                num_users = len(subjects)
                num_actions = len(actions)

                self.env.log.debug("Starting local loop. Subjects is %s",
                                   subjects)
                for user, action in local_permissions:

                    self.env.log.debug("local_check if %s in %s", user,
                                       subjects)
                    if user in subjects:
                        self.env.log.debug("YES! parsing %s - %s", user,
                                           action)

                        #Si el usuario aparece en la base de datos local, añadir
                        #a locally_processed para que no se busque en el maestro
                        locally_processed.add(user)

                        if action == 'INHERIT':
                            #Caso especial, Inherit from global. Añadimos a la lista inherit_pending
                            inherit_pending.add(user)

                        if action.isupper() and action not in actions:
                            self.env.log.debug("local: adding action %s",
                                               action)
                            actions.add(action)

                        if not action.isupper() and action not in subjects:
                            # action is actually the name of the permission group
                            # here
                            subjects.add(action)

                if num_users == len(subjects) and num_actions == len(actions):
                    break

            #After exhausting local expansion, try expanding global pending groups
            num_users = len(subjects)
            num_actions = len(actions)
            self.env.log.debug(
                "Starting global loop. locally_processed = %s, inherit_pending = %s",
                locally_processed, inherit_pending)
            self.env.log.debug("subjects = %s", subjects)
            for global_user, action in global_permissions:
                user = global_user[len(self.global_prefix):]
                pending_subjects = (subjects -
                                    locally_processed) | inherit_pending
                self.env.log.debug("global_check if %s in %s ", user,
                                   pending_subjects)
                if user in pending_subjects:
                    if action.isupper() and action not in actions:
                        self.env.log.debug("global: adding action %s ", action)
                        actions.add(action)
                    if not action.isupper() and action not in subjects:
                        # action is actually the name of the permission group
                        # here
                        subjects.add(action)
                        #Exit right now, to try to expand locally first
                        break

            if num_users == len(subjects) and num_actions == len(actions):
                break

        return list(actions)

        #IPermissionStore Methods

        return self.special_get_user_permissions(username)

    def get_users_with_permissions(self, permissions):

        #Need to replace the original code instead of calling it, because it uses
        #'self.get_users_permissions', which is not our 'get_users_permissions' function

        # get_user_permissions() takes care of the magic 'authenticated' group.
        # The optimized loop we had before didn't.  This is very inefficient,
        # but it works.
        result = set()
        users = set([u[0] for u in self.env.get_known_users()])
        for user in users:
            userperms = self.get_user_permissions(user)
            for perm in permissions:
                if perm in userperms:
                    result.add(user)
        return list(result)

    def get_all_permissions(self):

        #Permisos locales
        perms = self.store.get_all_permissions()

        env = get_master_env(self.env)
        if env != self.env:
            perms = perms + [(user, permission) \
            for user, permission in PermissionSystem(env).get_all_permissions() \
            if user.startswith(self.global_prefix)]

        return perms

    def grant_permission(self, username, action):

        if username.startswith(self.global_prefix):
            env = get_master_env(self.env)
            if env != self.env:
                assert False, "Global subjects can be modified only from Master Project"
                return

        env = get_master_env(self.env)
        return self.store.grant_permission(username, action)

    def revoke_permission(self, username, action):

        if username.startswith(self.global_prefix):
            env = get_master_env(self.env)
            if env != self.env:
                assert False, "Global subjects can be modified only from Master Project"
                return

        return self.store.revoke_permission(username, action)

    #IPermissionRequestor Methods
    def get_permission_actions(self):
        def expand_action(action):
            actions = set([])
            if isinstance(action, list):
                for a in action:
                    actions = actions | expand_action(a)
                return actions
            elif isinstance(action, tuple):
                actions.add(action[0])
                actions = actions | expand_action(action[1])
                return actions
            else:
                return set([action])

        try:
            if self.recursing: return []
        except:
            pass

        actions = ['NONE', 'INHERIT']

        #Work only in master project
        if self.env != get_master_env(self.env): return actions

        self.recursing = True
        known_actions = set(actions)
        for requestor in [
                r for r in PermissionSystem(self.env).requestors
                if r is not self
        ]:
            known_actions = known_actions | expand_action(
                requestor.get_permission_actions())
        try:
            del self.recursing
        except:
            pass

        projects = get_project_list(self.env)
        for project, project_path, project_url, env in projects:
            for requestor in [
                    r for r in PermissionSystem(env).requestors
                    if r is not self
            ]:
                #if requestor == self: continue
                for action in requestor.get_permission_actions():
                    #if action not in actions: actions.append(action)
                    if isinstance(action, tuple):
                        if action[0] not in known_actions:
                            actions.append(action)
                            known_actions.add(action[0])
                    else:
                        if action not in known_actions:
                            actions.append(action)
                            known_actions.add(action)

        #Return permissions from all projects if this is master project
        return list(actions)
Esempio n. 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 the underlying password storage
    implementation set in trac.ini with the "account-manager.password_format"
    setting.
    """

    implements(IAccountChangeListener)

    _password_store = ExtensionOption('account-manager', 'password_store',
                                      IPasswordStore)
    _password_format = Option('account-manager', 'password_format')
    stores = ExtensionPoint(IPasswordStore)
    change_listeners = ExtensionPoint(IAccountChangeListener)

    # Public API

    def get_users(self):
        return self.password_store.get_users()

    def has_user(self, user):
        return self.password_store.has_user(user)

    def set_password(self, user, password):
        if self.password_store.set_password(user, password):
            self._notify('created', user, password)
        else:
            self._notify('password_changed', user, password)

    def check_password(self, user, password):
        return self.password_store.check_password(user, password)

    def delete_user(self, user):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        # Delete session attributes
        cursor.execute("DELETE FROM session_attribute where sid=%s", (user, ))
        # Delete session
        cursor.execute("DELETE FROM session where sid=%s", (user, ))
        # Delete any custom permissions set for the user
        cursor.execute("DELETE FROM permission where username=%s", (user, ))
        db.commit()
        db.close()
        # Delete from password store
        if self.password_store.delete_user(user):
            self._notify('deleted', user)

    def supports(self, operation):
        try:
            store = self.password_store
        except AttributeError:
            return False
        else:
            return hasattr(store, operation)

    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 _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)
Esempio n. 20
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
Esempio n. 21
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)]
Esempio n. 22
0
class TracIStan(Component):
    """Handle the registered IStanRequestHandler(s)

    """
    implements(IRequestHandler, ITemplateProvider)
    stanreqhandlers = ExtensionPoint(IStanRequestHandler)

    default_handler = ExtensionOption(
        'tracistan', 'default_handler', IStanRequestHandler, '',
        """Name of the TracIStan component that handles requests to the base URL."""
    )

    def __init__(self):
        self.stantheman = StanEngine(self.env)

    # IRequestHandler methods
    def match_request(self, req):
        for handler in self.stanreqhandlers:
            if handler.match_request(req):
                return True
            continue
        return False

    def process_request(self, req):
        if not req.path_info or req.path_info == '/':
            chosen_handler = self.default_handler
        else:
            for handler in self.stanreqhandlers:
                if handler.match_request(req):
                    chosen_handler = handler
                    break
                continue
        req.standata = {}
        hdf = getattr(req, 'hdf', None)
        if hdf:
            req.standata['hdf'] = hdf
        template, content_type = chosen_handler.process_request(req)
        content_type = content_type or 'text/html'
        self._return(req, template, content_type)
        return None

    def _return(self, req, template, content_type='text/html'):
        """ Wrap the return so that things are processed by Stan
    
        """
        if req.args.has_key('hdfdump'):
            # FIXME: the administrator should probably be able to disable HDF
            #        dumps
            from pprint import PrettyPrinter
            outstream = StringIO()
            pp = PrettyPrinter(stream=outstream)
            pp.pprint(req.standata)
            content_type = 'text/plain'
            data = outstream.getvalue()
            outstream.close()
        else:
            ct = content_type.split('/')[0]
            data = self._render(req.standata, template)

        req.send_response(200)
        req.send_header('Cache-control', 'must-revalidate')
        req.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
        req.send_header('Content-Type', content_type + ';charset=utf-8')
        req.send_header('Content-Length', len(data))
        req.end_headers()

        if req.method != 'HEAD':
            req.write(data)
        pass

    def _render(self, data, template):
        c = Chrome(self.env)
        self.stantheman.template_dirs = c.get_all_templates_dirs()
        return self.stantheman.render(data, template=template)

    # ITemplateProvider
    def get_templates_dirs(self):
        """ Return the absolute path of the directory containing the provided
            templates

        """
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        """ Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a `(prefix, abspath)` tuple. The
        `prefix` part defines the path in the URL that requests to these
        resources are prefixed with.
        
        The `abspath` is the absolute path to the directory containing the
        resources on the local file system.

        """
        return []
Esempio n. 23
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))
Esempio n. 24
0
class TracIniAdminPanel(Component):
    """ An editor panel for trac.ini. """

    implements(IAdminPanelProvider, ITemplateProvider)

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

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

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

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

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

    DEFAULT_PASSWORD_OPTIONS = {'notification|smtp_password': True}

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

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

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

    #
    # IAdminPanelProvider methods
    #

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

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

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

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

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

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

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

        all_section_names.sort()

        sections = {}

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

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

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

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

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

            return None

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

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

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

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

                    if section_name == 'components':
                        continue

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        add_stylesheet(req, 'inieditorpanel/main.css')
        add_script(req, 'inieditorpanel/editor.js')
        return 'admin_tracini.html', data
Esempio n. 25
0
class RoadmapModule(Component):
    """Give an overview over all the milestones."""

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)

    stats_provider = ExtensionOption(
        'roadmap', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
        which is used to collect statistics on groups of tickets for display
        in the roadmap views.""")

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'roadmap'

    def get_navigation_items(self, req):
        if 'ROADMAP_VIEW' in req.perm:
            yield ('mainnav', 'roadmap',
                   tag.a(_('Roadmap'),
                         href=req.href.roadmap(),
                         accesskey=accesskey(req, 3)))

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = [
            'MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY',
            'MILESTONE_VIEW', 'ROADMAP_VIEW'
        ]
        return ['ROADMAP_VIEW'] + [('ROADMAP_ADMIN', actions)]

    # IRequestHandler methods

    def match_request(self, req):
        return req.path_info == '/roadmap'

    def process_request(self, req):
        req.perm.require('ROADMAP_VIEW')

        show = req.args.getlist('show')
        if 'all' in show:
            show = ['completed']

        milestones = Milestone.select(self.env, 'completed' in show)
        if 'noduedate' in show:
            milestones = [
                m for m in milestones if m.due is not None or m.completed
            ]
        milestones = [
            m for m in milestones if 'MILESTONE_VIEW' in req.perm(m.resource)
        ]

        stats = []
        queries = []

        all_tickets = get_tickets_for_all_milestones(self.env, field='owner')
        for milestone in milestones:
            tickets = all_tickets.get(milestone.name) or []
            tickets = apply_ticket_permissions(self.env, req, tickets)
            stat = get_ticket_stats(self.stats_provider, tickets)
            stats.append(
                milestone_stats_data(self.env, req, stat, milestone.name))
            # milestone['tickets'] = tickets  # for the iCalendar view

        if req.args.get('format') == 'ics':
            self._render_ics(req, milestones)
            return

        # FIXME should use the 'webcal:' scheme, probably
        username = None
        if req.is_authenticated:
            username = req.authname
        icshref = req.href.roadmap(show=show, user=username, format='ics')
        add_link(req, 'alternate', auth_link(req, icshref), _("iCalendar"),
                 'text/calendar', 'ics')

        data = {
            'milestones': milestones,
            'milestone_stats': stats,
            'queries': queries,
            'show': show,
        }
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'roadmap.html', data

    # Internal methods

    def _render_ics(self, req, milestones):
        req.send_response(200)
        req.send_header('Content-Type', 'text/calendar;charset=utf-8')
        buf = io.StringIO()

        from trac.ticket import Priority
        priorities = {}
        for priority in Priority.select(self.env):
            priorities[priority.name] = float(priority.value)

        def get_priority(ticket):
            value = priorities.get(ticket['priority'])
            if value:
                return int(
                    (len(priorities) + 8 * value - 9) / (len(priorities) - 1))

        def get_status(ticket):
            status = ticket['status']
            if status == 'new' or status == 'reopened' and \
                    not ticket['owner']:
                return 'NEEDS-ACTION'
            elif status in ('assigned', 'reopened'):
                return 'IN-PROCESS'
            elif status == 'closed':
                if ticket['resolution'] == 'fixed':
                    return 'COMPLETED'
                else:
                    return 'CANCELLED'
            else:
                return ''

        def escape_value(text):
            s = ''.join(map(lambda c: '\\' + c if c in ';,\\' else c, text))
            return '\\n'.join(re.split(r'[\r\n]+', s))

        def write_prop(name, value, params={}):
            text = ';'.join([name] + [k + '=' + v for k, v
                                                  in params.items()]) + \
                   ':' + escape_value(value)
            firstline = 1
            text = to_unicode(text)
            while text:
                if not firstline:
                    text = ' ' + text
                else:
                    firstline = 0
                buf.write(text[:75] + CRLF)
                text = text[75:]

        def write_date(name, value, params={}):
            params['VALUE'] = 'DATE'
            write_prop(name, format_date(value, '%Y%m%d', req.tz), params)

        def write_utctime(name, value, params={}):
            write_prop(name, format_datetime(value, '%Y%m%dT%H%M%SZ', utc),
                       params)

        host = req.base_url[req.base_url.find('://') + 3:]
        user = req.args.get('user', 'anonymous')

        write_prop('BEGIN', 'VCALENDAR')
        write_prop('VERSION', '2.0')
        write_prop(
            'PRODID', '-//Edgewall Software//NONSGML Trac %s//EN' %
            self.env.trac_version)
        write_prop('METHOD', 'PUBLISH')
        write_prop('X-WR-CALNAME',
                   self.env.project_name + ' - ' + _("Roadmap"))
        write_prop('X-WR-CALDESC', self.env.project_description)
        write_prop('X-WR-TIMEZONE', str(req.tz))

        all_tickets = get_tickets_for_all_milestones(self.env, field='owner')
        for milestone in milestones:
            uid = '<%s/milestone/%s@%s>' % (req.base_path, milestone.name,
                                            host)
            if milestone.due:
                write_prop('BEGIN', 'VEVENT')
                write_prop('UID', uid)
                write_utctime('DTSTAMP', milestone.due)
                write_date('DTSTART', milestone.due)
                write_prop('SUMMARY',
                           _("Milestone %(name)s", name=milestone.name))
                write_prop('URL', req.abs_href.milestone(milestone.name))
                if milestone.description:
                    write_prop('DESCRIPTION', milestone.description)
                write_prop('END', 'VEVENT')
            tickets = all_tickets.get(milestone.name) or []
            tickets = apply_ticket_permissions(self.env, req, tickets)
            for tkt_id in [
                    ticket['id'] for ticket in tickets
                    if ticket['owner'] == user
            ]:
                ticket = Ticket(self.env, tkt_id)
                write_prop('BEGIN', 'VTODO')
                write_prop('UID',
                           '<%s/ticket/%s@%s>' % (req.base_path, tkt_id, host))
                if milestone.due:
                    write_prop('RELATED-TO', uid)
                    write_date('DUE', milestone.due)
                write_prop(
                    'SUMMARY',
                    _("Ticket #%(num)s: %(summary)s",
                      num=ticket.id,
                      summary=ticket['summary']))
                write_prop('URL', req.abs_href.ticket(ticket.id))
                write_prop('DESCRIPTION', ticket['description'])
                priority = get_priority(ticket)
                if priority:
                    write_prop('PRIORITY', unicode(priority))
                write_prop('STATUS', get_status(ticket))
                if ticket['status'] == 'closed':
                    for time, in self.env.db_query(
                            """
                            SELECT time FROM ticket_change
                            WHERE ticket=%s AND field='status'
                            ORDER BY time desc LIMIT 1
                            """, (ticket.id, )):
                        write_utctime('COMPLETED', from_utimestamp(time))
                write_prop('END', 'VTODO')
        write_prop('END', 'VCALENDAR')

        ics_str = buf.getvalue().encode('utf-8')
        req.send_header('Content-Length', len(ics_str))
        req.end_headers()
        req.write(ics_str)
        raise RequestDone
Esempio n. 26
0
class SmpVersionProject(Component):
    """Create Project dependent versions"""

    implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter)

    stats_provider = ExtensionOption(
        'roadmap', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`, 
        which is used to collect statistics on groups of tickets for display
        in the roadmap views.""")

    def __init__(self):
        self.__SmpModel = SmpModel(self.env)

    # IRequestHandler methods
    def match_request(self, req):
        match = re.match(r'/version(?:/(.+))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['id'] = match.group(1)
            return True

    def process_request(self, req):
        version_id = req.args.get('id')
        version_project = req.args.get('project', '')

        db = self.env.get_db_cnx()  # TODO: db can be removed
        action = req.args.get('action', 'view')
        try:
            version = Version(self.env, version_id, db)
        except:
            version = Version(self.env, None, db)
            version.name = version_id
            action = 'edit'  # rather than 'new' so that it works for POST/save

        if req.method == 'POST':
            if req.args.has_key('cancel'):
                if version.exists:
                    req.redirect(req.href.version(version.name))
                else:
                    req.redirect(req.href.roadmap())
            elif action == 'edit':
                return self._do_save(req, db, version)
            elif action == 'delete':
                self._do_delete(req, version)
        elif action in ('new', 'edit'):
            return self._render_editor(req, db, version)
        elif action == 'delete':
            return self._render_confirm(req, db, version)

        if not version.name:
            req.redirect(req.href.roadmap())

        return self._render_view(req, db, version)

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        if req.path_info.startswith('/admin/ticket/versions'):
            if req.method == 'POST':
                versions = req.args.get('sel')
                remove = req.args.get('remove')
                save = req.args.get('save')
                if not remove is None and not versions is None:
                    if type(versions) is list:
                        for version in versions:
                            self.__SmpModel.delete_version_project(version)
                    else:
                        self.__SmpModel.delete_version_project(versions)
                elif not save is None:
                    match = re.match(r'/admin/ticket/versions/(.+)$',
                                     req.path_info)
                    if match and match.group(1) != req.args.get('name'):
                        self.__SmpModel.rename_version_project(
                            match.group(1), req.args.get('name'))

        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.path_info.startswith('/roadmap'):
            hide = smp_settings(req, 'roadmap', 'hide', None)
            filter_projects = smp_filter_settings(req, 'roadmap', 'projects')

            if hide:
                data['hide'] = hide

            if not hide or 'versions' not in hide:
                versions, version_stats = self._versions_and_stats(
                    req, filter_projects)
                data['versions'] = versions
                data['version_stats'] = version_stats

            if hide and 'milestones' in hide:
                data['milestones'] = []
                data['milestone_stats'] = []

            return "roadmap_versions.html", data, content_type

        return template, data, content_type

    # ITemplateStreamFilter methods

    def filter_stream(self, req, method, filename, stream, data):
        action = req.args.get('action', 'view')

        if filename == "version_edit.html":
            if action == 'new':
                filter = Transformer('//form[@id="edit"]/div[1]')
                return stream | filter.before(self.__new_project())
            elif action == 'edit':
                filter = Transformer('//form[@id="edit"]/div[1]')
                return stream | filter.before(self.__edit_project(data))

        return stream

    # Internal methods

    def __edit_project(self, data):
        version = data.get('version').name
        all_projects = self.__SmpModel.get_all_projects()
        id_project_version = self.__SmpModel.get_id_project_version(version)

        if id_project_version != None:
            id_project_selected = id_project_version[0]
        else:
            id_project_selected = None

        return tag.div(tag.label(
            'Project:', tag.br(),
            tag.select(tag.option(), [
                tag.option(row[1],
                           selected=(id_project_selected == row[0] or None),
                           value=row[0])
                for row in sorted(all_projects, key=itemgetter(1))
            ],
                       name="project")),
                       class_="field")

    def __new_project(self):
        all_projects = self.__SmpModel.get_all_projects()

        return tag.div(tag.label(
            'Project:', tag.br(),
            tag.select(tag.option(), [
                tag.option(row[1], value=row[0])
                for row in sorted(all_projects, key=itemgetter(1))
            ],
                       name="project")),
                       class_="field")

    def _do_delete(self, req, version):
        req.perm.require('MILESTONE_DELETE')
        version_name = version.name
        version.delete()

        self.__SmpModel.delete_version_project(version_name)

        add_notice(
            req,
            _('The version "%(name)s" has been deleted.', name=version_name))
        req.redirect(req.href.roadmap())

    def _do_save(self, req, db, version):
        version_name = req.args.get('name')
        version_project = req.args.get('project')
        old_version_project = self.__SmpModel.get_id_project_version(
            version.name)

        if version.exists:
            req.perm.require('MILESTONE_MODIFY')
        else:
            req.perm.require('MILESTONE_CREATE')

        old_name = version.name
        new_name = version_name

        version.description = req.args.get('description', '')

        time = req.args.get('time', '')
        if time:
            version.time = user_time(req, parse_date, time, hint='datetime')
        else:
            version.time = None

        # Instead of raising one single error, check all the constraints and
        # let the user fix them by going back to edit mode showing the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        # -- check the name
        # If the name has changed, check that the version doesn't already
        # exist
        # FIXME: the whole .exists business needs to be clarified
        #        (#4130) and should behave like a WikiPage does in
        #        this respect.
        try:
            new_version = Version(self.env, new_name, db)
            if new_version.name == old_name:
                pass  # Creation or no name change
            elif new_version.name:
                warn(
                    _(
                        'Version "%(name)s" already exists, please '
                        'choose another name.',
                        name=new_version.name))
            else:
                warn(_('You must provide a name for the version.'))
        except:
            version.name = new_name

        if warnings:
            return self._render_editor(req, db, version)

        # -- actually save changes

        if version.exists:
            version.update()

            if old_name != version.name:
                self.__SmpModel.rename_version_project(old_name, version.name)

            if not version_project:
                self.__SmpModel.delete_version_project(version.name)
            elif not old_version_project:
                self.__SmpModel.insert_version_project(version.name,
                                                       version_project)
            else:
                self.__SmpModel.update_version_project(version.name,
                                                       version_project)
        else:
            version.insert()
            if version_project:
                self.__SmpModel.insert_version_project(version.name,
                                                       version_project)

        add_notice(req, _('Your changes have been saved.'))
        req.redirect(req.href.version(version.name))

    def _render_confirm(self, req, db, version):
        req.perm.require('MILESTONE_DELETE')

        version = [
            v for v in Version.select(self.env, db=db)
            if v.name != version.name and 'MILESTONE_VIEW' in req.perm
        ]
        data = {'version': version}
        return 'version_delete.html', data, None

    def _render_editor(self, req, db, version):
        # Suggest a default due time of 18:00 in the user's timezone
        default_time = datetime.now(req.tz).replace(hour=18,
                                                    minute=0,
                                                    second=0,
                                                    microsecond=0)
        if default_time <= datetime.now(utc):
            default_time += timedelta(days=1)

        data = {
            'version': version,
            'datetime_hint': get_datetime_format_hint(),
            'default_time': default_time
        }

        if version.exists:
            req.perm.require('MILESTONE_MODIFY')
            versions = [
                v for v in Version.select(self.env, db=db)
                if v.name != version.name and 'MILESTONE_VIEW' in req.perm
            ]
        else:
            req.perm.require('MILESTONE_CREATE')

        Chrome(self.env).add_wiki_toolbars(req)
        return 'version_edit.html', data, None

    def _render_view(self, req, db, version):
        version_groups = []
        available_groups = []
        component_group_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

        # collect fields that can be used for grouping
        for field in ticket_fields:
            if field['type'] == 'select' and field['name'] != 'version' \
                    or field['name'] in ('owner', 'reporter'):
                available_groups.append({
                    'name': field['name'],
                    'label': field['label']
                })
                if field['name'] == 'component':
                    component_group_available = True

        # determine the field currently used for grouping
        by = None
        if component_group_available:
            by = 'component'
        elif available_groups:
            by = available_groups[0]['name']
        by = req.args.get('by', by)

        tickets = get_tickets_for_any(self.env, db, 'version', version.name,
                                      by)
        tickets = apply_ticket_permissions(self.env, req, tickets)
        stat = get_ticket_stats(self.stats_provider, tickets)

        context = Context.from_request(req)

        infodivclass = ''
        if VERSION <= '0.12':
            infodivclass = 'info'
        else:
            infodivclass = 'info trac-progress'

        data = {
            'context': context,
            'version': version,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'available_groups': available_groups,
            'grouped_by': by,
            'groups': version_groups,
            'infodivclass': infodivclass
        }
        data.update(
            any_stats_data(self.env, req, stat, 'version', version.name))

        if by:
            groups = []
            for field in ticket_fields:
                if field['name'] == by:
                    if 'options' in field:
                        groups = field['options']
                        if field.get('optional'):
                            groups.insert(0, '')
                    else:
                        cursor = db.cursor()
                        cursor.execute(
                            """
                            SELECT DISTINCT COALESCE(%s,'') FROM ticket
                            ORDER BY COALESCE(%s,'')
                            """, [by, by])
                        groups = [row[0] for row in cursor]

            max_count = 0
            group_stats = []

            for group in groups:
                values = group and (group, ) or (None, group)
                group_tickets = [t for t in tickets if t[by] in values]
                if not group_tickets:
                    continue

                gstat = get_ticket_stats(self.stats_provider, group_tickets)
                if gstat.count > max_count:
                    max_count = gstat.count

                group_stats.append(gstat)

                gs_dict = {'name': group}
                gs_dict.update(
                    any_stats_data(self.env, req, gstat, 'version',
                                   version.name, by, group))
                version_groups.append(gs_dict)

            for idx, gstat in enumerate(group_stats):
                gs_dict = version_groups[idx]
                percent = 1.0
                if max_count:
                    percent = float(gstat.count) / float(max_count) * 100
                gs_dict['percent_of_max_total'] = percent

        add_stylesheet(req, 'common/css/roadmap.css')
        add_script(req, 'common/js/folding.js')
        return 'version_view.html', data, None

    def _version_time(self, version):
        if version.time:
            return version.time.replace(tzinfo=None).strftime('%Y%m%d%H%M%S')
        else:
            return datetime(9999, 12,
                            31).strftime('%Y%m%d%H%M%S') + version.name

    def _versions_and_stats(self, req, filter_projects):
        req.perm.require('MILESTONE_VIEW')
        db = self.env.get_db_cnx()

        versions = Version.select(self.env, db)

        filtered_versions = []
        stats = []

        show = req.args.getlist('show')

        for version in sorted(versions, key=lambda v: self._version_time(v)):
            project = self.__SmpModel.get_project_version(version.name)

            if not filter_projects or (project
                                       and project[0] in filter_projects):
                if not version.time or version.time.replace(
                        tzinfo=None) >= datetime.now() or 'completed' in show:

                    if version.time:
                        if version.time.replace(tzinfo=None) >= datetime.now():
                            version.is_due = True
                        else:
                            version.is_completed = True

                    filtered_versions.append(version)
                    tickets = get_tickets_for_any(self.env, db, 'version',
                                                  version.name, 'owner')
                    tickets = apply_ticket_permissions(self.env, req, tickets)
                    stat = get_ticket_stats(self.stats_provider, tickets)
                    stats.append(
                        any_stats_data(self.env, req, stat, 'version',
                                       version.name))

        return filtered_versions, stats
Esempio n. 27
0
File: main.py Progetto: 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
Esempio n. 28
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'')
        """)

    # 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]
Esempio n. 29
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']
Esempio n. 30
0
class NotificationSystem(Component):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        See TracNotification for more details.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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