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)
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
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.')
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)
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()
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)
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)
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
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)]
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)
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')]
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)]
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
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
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
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
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)
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)
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
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)]
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 []
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))
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
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
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
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
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]
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']
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)