def resolve_ep_class(interface, component, clsnm, **kwargs): r"""Retrieve the class implementing an interface (by name) """ ep = ExtensionPoint(interface) for c in ep.extensions(component): if c.__class__.__name__ == clsnm : return c else: if 'default' in kwargs: return kwargs['default'] else: raise LookupError('No match found for class %s implementing %s' % (clsnm, interface) )
class OrderedExtensionsOption(ListOption): """A comma separated, ordered, list of components implementing `interface`. Can be empty. If `include_missing` is true (the default) all components implementing the interface are returned, with those specified by the option ordered first.""" def __init__(self, section, name, interface, default=None, include_missing=True, doc=''): ListOption.__init__(self, section, name, default, doc=doc) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing def __get__(self, instance, owner): if instance is None: return self order = ListOption.__get__(self, instance, owner) components = [] for impl in self.xtnpt.extensions(instance): if self.include_missing or impl.__class__.__name__ in order: components.append(impl) def compare(x, y): x, y = x.__class__.__name__, y.__class__.__name__ if x not in order: return int(y in order) if y not in order: return -int(x in order) return cmp(order.index(x), order.index(y)) components.sort(compare) return components
class ExtensionOption(Option): """Name of a component implementing `interface`. Raises a `ConfigurationError` if the component cannot be found in the list of active components implementing the interface.""" def __init__(self, section, name, interface, default=None, doc='', doc_domain='tracini'): Option.__init__(self, section, name, default, doc, doc_domain) self.xtnpt = ExtensionPoint(interface) def __get__(self, instance, owner): if instance is None: return self value = Option.__get__(self, instance, owner) for impl in self.xtnpt.extensions(instance): if impl.__class__.__name__ == value: return impl raise ConfigurationError( tag_("Cannot find an implementation of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.code(self.xtnpt.interface.__name__), implementation=tag.code(value), option=tag.code("[%s] %s" % (self.section, self.name))))
class OrderedExtensionsOption(ListOption): """A comma separated, ordered, list of components implementing `interface`. Can be empty. If `include_missing` is true (the default) all components implementing the interface are returned, with those specified by the option ordered first. """ def __init__(self, section, name, interface, default=None, include_missing=True, doc='', doc_domain='tracini'): ListOption.__init__(self, section, name, default, doc=doc, doc_domain=doc_domain) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing def __get__(self, instance, owner): if instance is None: return self order = ListOption.__get__(self, instance, owner) components = [] implementing_classes = [] for impl in self.xtnpt.extensions(instance): implementing_classes.append(impl.__class__.__name__) if self.include_missing or impl.__class__.__name__ in order: components.append(impl) not_found = sorted(set(order) - set(implementing_classes)) if not_found: raise ConfigurationError( tag_("Cannot find implementation(s) of the %(interface)s " "interface named %(implementation)s. Please check " "that the Component is enabled or update the option " "%(option)s in trac.ini.", interface=tag.code(self.xtnpt.interface.__name__), implementation=tag( (', ' if idx != 0 else None, tag.code(impl)) for idx, impl in enumerate(not_found)), option=tag.code("[%s] %s" % (self.section, self.name)))) def compare(x, y): x, y = x.__class__.__name__, y.__class__.__name__ if x not in order: return int(y in order) if y not in order: return -int(x in order) return cmp(order.index(x), order.index(y)) components.sort(compare) return components
class ExtensionOption(Option): def __init__(self, section, name, interface, default=None, doc=''): Option.__init__(self, section, name, default, doc) self.xtnpt = ExtensionPoint(interface) def __get__(self, instance, owner): if instance is None: return self value = Option.__get__(self, instance, owner) for impl in self.xtnpt.extensions(instance): if impl.__class__.__name__ == value: return impl raise AttributeError('Cannot find an implementation of the "%s" ' 'interface named "%s". Please update the option ' '%s.%s in trac.ini.' % (self.xtnpt.interface.__name__, value, self.section, self.name))
class EmailDistributor(Component): """Distributes notification events as emails.""" implements(INotificationDistributor) formatters = ExtensionPoint(INotificationFormatter) decorators = ExtensionPoint(IEmailDecorator) resolvers = OrderedExtensionsOption( 'notification', 'email_address_resolvers', IEmailAddressResolver, 'SessionEmailResolver', include_missing=False, doc="""Comma separated list of email resolver components in the order they will be called. If an email address is resolved, the remaining resolvers will not be called. """) default_format = Option( 'notification', 'default_format.email', 'text/plain', doc="Default format to distribute email notifications.") def __init__(self): self._charset = create_charset( self.config.get('notification', 'mime_encoding')) # INotificationDistributor methods def transports(self): yield 'email' def distribute(self, transport, recipients, event): if transport != 'email': return if not self.config.getbool('notification', 'smtp_enabled'): self.log.debug("%s skipped because smtp_enabled set to false", self.__class__.__name__) return formats = {} for f in self.formatters: for style, realm in f.get_supported_styles(transport): if realm == event.realm: formats[style] = f if not formats: self.log.error("%s No formats found for %s %s", self.__class__.__name__, transport, event.realm) return self.log.debug( "%s has found the following formats capable of " "handling '%s' of '%s': %s", self.__class__.__name__, transport, event.realm, ', '.join(formats)) matcher = RecipientMatcher(self.env) notify_sys = NotificationSystem(self.env) always_cc = set(notify_sys.smtp_always_cc_list) addresses = {} for sid, auth, addr, fmt in recipients: if fmt not in formats: self.log.debug("%s format %s not available for %s %s", self.__class__.__name__, fmt, transport, event.realm) continue if sid and not addr: for resolver in self.resolvers: addr = resolver.get_address_for_session(sid, auth) or None if addr: self.log.debug( "%s found the address '%s' for '%s [%s]' via %s", self.__class__.__name__, addr, sid, auth, resolver.__class__.__name__) break if sid and auth and not addr: addr = sid if notify_sys.smtp_default_domain and \ not notify_sys.use_short_addr and \ addr and matcher.nodomaddr_re.match(addr): addr = '%s@%s' % (addr, notify_sys.smtp_default_domain) if not addr: self.log.debug( "%s was unable to find an address for " "'%s [%s]'", self.__class__.__name__, sid, auth) elif matcher.is_email(addr) or \ notify_sys.use_short_addr and \ matcher.nodomaddr_re.match(addr): addresses.setdefault(fmt, set()).add(addr) if sid and auth and sid in always_cc: always_cc.discard(sid) always_cc.add(addr) elif notify_sys.use_public_cc: always_cc.add(addr) else: self.log.debug( "%s was unable to use an address '%s' for '%s " "[%s]'", self.__class__.__name__, addr, sid, auth) outputs = {} failed = [] for fmt, formatter in formats.iteritems(): if fmt not in addresses and fmt != 'text/plain': continue try: outputs[fmt] = formatter.format(transport, fmt, event) except Exception as e: self.log.warning( '%s caught exception while ' 'formatting %s to %s for %s: %s%s', self.__class__.__name__, event.realm, fmt, transport, formatter.__class__, exception_to_unicode(e, traceback=True)) failed.append(fmt) # Fallback to text/plain when formatter is broken if failed and 'text/plain' in outputs: for fmt in failed: addresses.setdefault('text/plain', set()) \ .update(addresses.pop(fmt, ())) for fmt, addrs in addresses.iteritems(): self.log.debug("%s is sending event as '%s' to: %s", self.__class__.__name__, fmt, ', '.join(addrs)) message = self._create_message(fmt, outputs) if message: addrs = set(addrs) cc_addrs = sorted(addrs & always_cc) bcc_addrs = sorted(addrs - always_cc) self._do_send(transport, event, message, cc_addrs, bcc_addrs) else: self.log.warning("%s cannot send event '%s' as '%s': %s", self.__class__.__name__, event.realm, fmt, ', '.join(addrs)) def _create_message(self, format, outputs): if format not in outputs: return None message = create_mime_multipart('related') maintype, subtype = format.split('/') preferred = create_mime_text(outputs[format], subtype, self._charset) if format != 'text/plain' and 'text/plain' in outputs: alternative = create_mime_multipart('alternative') alternative.attach( create_mime_text(outputs['text/plain'], 'plain', self._charset)) alternative.attach(preferred) preferred = alternative message.attach(preferred) return message def _do_send(self, transport, event, message, cc_addrs, bcc_addrs): notify_sys = NotificationSystem(self.env) smtp_from = notify_sys.smtp_from smtp_from_name = notify_sys.smtp_from_name or self.env.project_name smtp_replyto = notify_sys.smtp_replyto if not notify_sys.use_short_addr and notify_sys.smtp_default_domain: if smtp_from and '@' not in smtp_from: smtp_from = '%s@%s' % (smtp_from, notify_sys.smtp_default_domain) if smtp_replyto and '@' not in smtp_replyto: smtp_replyto = '%s@%s' % (smtp_replyto, notify_sys.smtp_default_domain) headers = {} headers['X-Mailer'] = 'Trac %s, by Edgewall Software'\ % self.env.trac_version headers['X-Trac-Version'] = self.env.trac_version headers['X-Trac-Project'] = self.env.project_name headers['X-URL'] = self.env.project_url headers['X-Trac-Realm'] = event.realm headers['Precedence'] = 'bulk' headers['Auto-Submitted'] = 'auto-generated' if isinstance(event.target, (list, tuple)): targetid = ','.join(map(get_target_id, event.target)) else: targetid = get_target_id(event.target) rootid = create_message_id(self.env, targetid, smtp_from, None, more=event.realm) if event.category == 'created': headers['Message-ID'] = rootid else: headers['Message-ID'] = create_message_id(self.env, targetid, smtp_from, event.time, more=event.realm) headers['In-Reply-To'] = rootid headers['References'] = rootid headers['Date'] = formatdate() headers['From'] = (smtp_from_name, smtp_from) \ if smtp_from_name else smtp_from headers['To'] = 'undisclosed-recipients: ;' if cc_addrs: headers['Cc'] = ', '.join(cc_addrs) if bcc_addrs: headers['Bcc'] = ', '.join(bcc_addrs) headers['Reply-To'] = smtp_replyto for k, v in headers.iteritems(): set_header(message, k, v, self._charset) for decorator in self.decorators: decorator.decorate_message(event, message, self._charset) from_name, from_addr = parseaddr(str(message['From'])) to_addrs = set() for name in ('To', 'Cc', 'Bcc'): values = map(str, message.get_all(name, ())) to_addrs.update(addr for name, addr in getaddresses(values) if addr) del message['Bcc'] notify_sys.send_email(from_addr, list(to_addrs), message.as_string())
class TracPasswordStoreUser(Component): tracpasswordstore_observers = ExtensionPoint(IPasswordStore) @tracob_first def has_user(self, *_args, **_kw): return self.tracpasswordstore_observers
def __init__(self, section, name, interface, default=None, doc='', doc_domain='tracini', doc_args=None): Option.__init__(self, section, name, default, doc, doc_domain, doc_args) self.xtnpt = ExtensionPoint(interface)
class PermissionsAdminPanel(Component): implements(IAdminPanelProvider) # Extension points project_change_listeners = ExtensionPoint(IProjectChangeListener) # list in order in which they should be listed in the UI MEMBER_TYPES = ('login_status', 'user', 'organization', 'ldap') def __init__(self): self.conf = Configuration.instance() # IAdminPanelProvider methods def get_admin_panels(self, req): if 'PERMISSION_GRANT' in req.perm or 'PERMISSION_REVOKE' in req.perm: yield ('general', _('General'), 'permissions', _('Permissions')) def render_admin_panel(self, req, cat, page, path_info): add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/permissions.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') add_stylesheet(req, 'multiproject/css/permissions.css') project = Project.get(self.env) # is_normal_project = self.env.project_identifier != \ self.env.config.get('multiproject', 'sys_home_project_name') # API instances perm_sys = PermissionSystem(self.env) group_store = CQDEUserGroupStore(env=self.env) org_store = CQDEOrganizationStore.instance() if is_normal_project: membership = MembershipApi(self.env, Project.get(self.env)) else: membership = None if req.method == 'POST': action = req.args.get('action') if action == 'remove_member': self._remove_member(req, group_store) elif action == 'add_member': add_type = req.args.get('add_type') if add_type == 'user': self._add_user(req, group_store, membership) elif add_type == 'organization': self._add_organization(req, group_store) elif add_type == 'ldap_group': self._add_ldap_group(req, group_store) elif add_type == 'login_status': login_status = req.args.get('login_status') if login_status not in ('authenticated', 'anonymous'): raise TracError('Invalid arguments') self._add_user(req, group_store, membership, username=login_status) else: raise TracError('Invalid add_type') elif action == 'add_permission': self._add_perm_to_group(req, group_store, perm_sys) elif action == 'remove_permission': self._remove_permission(req, group_store, perm_sys) elif action == 'create_group': self._create_group(req, group_store, perm_sys) elif action == 'remove_group': self._remove_group(req, group_store) elif action == 'add_organization': self._add_organization(req, group_store) elif action == 'decline_membership': self._decline_membership(req, membership) elif 'makepublic' in req.args: project_api = Projects() if conf.allow_public_projects: self._make_public(req, project) project_api.add_public_project_visibility(project.id) # Reload page return req.redirect(req.href(req.path_info)) else: raise TracError("Public projects are disabled", "Error!") elif 'makeprivate' in req.args: project_api = Projects() self._make_private(req, project) project_api.remove_public_project_visibility(project.id) # Reload page return req.redirect(req.href(req.path_info)) else: raise TracError('Unknown action %s' % action) # get membership request list after form posts have been processed if is_normal_project: membership_requests = set(membership.get_membership_requests()) else: membership_requests = set() permissions = set(perm_sys.get_actions()) # check if project if current configuration and permission state is in such state that # permission editions are likely fail invalid_state = None if is_normal_project: is_a_public = project.public else: is_a_public = "" try: group_store.is_valid_group_members() except InvalidPermissionsState, e: add_warning( req, _('Application permission configuration conflicts with project permissions. ' 'Before you can fully edit permissions or users you will need to either remove ' 'offending permissions or set correct application configuration. Page reload' 'is required to update this warning.')) add_warning(req, e.message) return 'permissions.html', { 'perm_data': self._perm_data(group_store, perm_sys), 'theme_htdocs_location': self.env.config.get('multiproject', 'theme_htdocs_location', '/htdocs/theme'), 'permissions': sorted(permissions), 'organizations': sorted([org.name for org in org_store.get_organizations()]), 'use_organizations': self.config.getbool('multiproject-users', 'use_organizations', False), 'use_ldap': self.config.getbool('multiproject', 'ldap_groups_enabled', False), 'membership_requests': membership_requests, 'invalid_state': invalid_state, 'is_public': is_a_public, 'allow_public_projects': conf.allow_public_projects }
class UserProfilesListMacro(WikiMacroBase): """Returns project's team roster. Usage: {{{ # Without arguments returns current active user profiles (with enabled='1') [[UserProfilesList]] # Returns all userProfiles with role='developer' and enabled='1' [[UserProfilesList(role='developer', enabled='1')]] # Returns all userProfiles with name like 'someName' [[UserProfilesList(name='%someName%')]] # Returns cbalan's profile and user profiles with role='%arh%' [[UserProfilesList({id='cbalan'},{role='%arh%'})]] # Adds style and class attributes to box layout [[UserProfilesList(|class=someCSS_Class, style=border:1px solid green;padding:12px)]] }}} """ cells_providers = ExtensionPoint(IUserProfilesListMacroCellContributor) def expand_macro(self, formatter, name, content): env = self.env req = formatter.req content_args = {} data = dict(user_profiles=[], user_profile_fields={}) layout_args = {} rendered_result = "" user_profile_templates = [] # collecting arguments if content: for i, macro_args in enumerate(content.split('|')): if i == 0: content_args = MacroArguments(macro_args) continue if i == 1: layout_args = MacroArguments(macro_args) break # extracting userProfile attrs if len(content_args) > 0: user_profile_templates.append(User(**content_args)) if len(content_args.get_list_args()) > 0: for list_item in content_args.get_list_args(): user_profile_templates.append( User(**MacroArguments(list_item[1:len(list_item) - 1]))) # adding profiles fields description data['user_profile_fields'].update( UserProfileManager(env).get_user_profile_fields( ignore_internal=True)) # removing picture_href data['user_profile_fields'].pop('picture_href') def inline_wiki_to_html(text): return wiki_to_html(text, env, req) data['wiki_to_html'] = inline_wiki_to_html # grabbing users if len(user_profile_templates) > 0: data['user_profiles'] = UserManager(env).search_users( user_profile_templates) else: data['user_profiles'] = UserManager(env).get_active_users() data['cells'] = list(self._get_cells(req, data['user_profiles'])) # add stylesheet&script add_script(req, 'tracusermanager/js/macros_um_profile.js') add_stylesheet(req, 'tracusermanager/css/macros_um_profile.css') # render template template = Chrome(env).load_template('macro_um_profile.html', method='xhtml') data = Chrome(env).populate_data(req, {'users': data}) rendered_result = template.generate(**data) # wrap everything if len(layout_args) > 0: rendered_result = html.div(rendered_result, **layout_args) return rendered_result def _get_cells(self, req, user_list): for provider in self.cells_providers: for cell, label, order in provider.get_userlistmacro_cells(): if label == 'Email': cell = Chrome(self.env).format_author(req, cell) yield dict(name=cell, label=label, order=order, render_method=provider.render_userlistmacro_cell)
class PrivateReports(Component): group_providers = ExtensionPoint(IPermissionGroupProvider) implements(ITemplateStreamFilter, IEnvironmentSetupParticipant, IAdminPanelProvider, ITemplateProvider, IRequestFilter, IPermissionRequestor) ### IRequestFilter methods def pre_process_request(self, req, handler): if not isinstance(handler, ReportModule): return handler report_id = req.args.get('id') if not report_id or self._has_permission(req.authname, report_id): return handler else: self.log.debug("User %s doesn't have permission to view report %s " % (req.authname, report_id)) raise TracError("You don't have permission to access this report", "No Permission") def post_process_request(self, req, template, data, content_type): return template, data, content_type ### ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] ### IAdminPanelProvider methods def get_admin_panels(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('reports', 'Reports', 'privatereports', 'Private Reports') def render_admin_panel(self, req, cat, page, path_info): if page == 'privatereports': reports = self._get_reports() data = { 'reports': reports } if req.method == 'POST': report_id = req.args.get('report_id') try: report_id = int(report_id) except ValueError: req.redirect(self.env.href.admin.reports('privatereports')) if req.args.get('add'): new_permission = req.args.get('newpermission') if new_permission is None or \ new_permission.isupper() is False: req.redirect( self.env.href.admin.reports('privatereports')) self._insert_report_permission(report_id, new_permission) data['report_permissions'] = \ self._get_report_permissions(report_id) or '' data['show_report'] = report_id elif req.args.get('remove'): arg_report_permissions = req.args.get('report_permissions') if arg_report_permissions is None: req.redirect( self.env.href.admin.reports('privatereports')) report_permissions = \ self._get_report_permissions(report_id) report_permissions = set(report_permissions) to_remove = set() if type(arg_report_permissions) in StringTypes: to_remove.update([arg_report_permissions]) elif type(arg_report_permissions) == ListType: to_remove.update(arg_report_permissions) else: req.redirect( self.env.href.admin.reports('privatereports')) report_permissions = report_permissions - to_remove self._alter_report_permissions(report_id, report_permissions) data['report_permissions'] = report_permissions or '' data['show_report'] = report_id elif req.args.get('show'): report_permissions = \ self._get_report_permissions(report_id) data['report_permissions'] = report_permissions or '' data['show_report'] = report_id else: report_permissions = \ self._get_report_permissions(reports[0][1]) data['report_permissions'] = report_permissions or '' return 'admin_privatereports.html', data ### IEnvironmentSetupParticipant methods def environment_created(self): db = self.env.get_db_cnx() if self.environment_needs_upgrade(db): self.upgrade_environment(db) def environment_needs_upgrade(self, db): cursor = db.cursor() try: cursor.execute("SELECT report_id, permission FROM private_report") return False except: return True def upgrade_environment(self, db): cursor = db.cursor() try: cursor.execute("DROP TABLE IF EXISTS private_report") db.commit() except: cursor.connection.rollback() try: cursor = db.cursor() cursor.execute(""" CREATE TABLE private_report(report_id integer, permission text)""") db.commit() except: cursor.connection.rollback() ### ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if not filename == 'report_list.html': return stream stream_buffer = StreamBuffer() from pkg_resources import parse_version if parse_version(trac_version) < parse_version('1.0'): delimiter = '</tr>' selector = '//tbody/tr' else: delimiter = '</div>' selector = '//div[@class="reports"]/div' def check_report_permission(): report_stream = str(stream_buffer) reports_raw = report_stream.split(delimiter) report_stream = '' for row in reports_raw: if row is not None and len(row) != 0 and 'View report' in row: # determine the report id s = row.find('/report/') if s == -1: continue e = row.find('\"', s) if e == -1: continue report_id = row[s+len('/report/'):e] if self._has_permission(req.authname, report_id): report_stream += row else: self.log.debug("Removing report %s from list because " "%s doesn't have permission to view" % (report_id, req.authname)) elif 'View report' in row: report_stream += row return HTML(report_stream) return stream | Transformer(selector) \ .copy(stream_buffer) \ .replace(check_report_permission) ### IPermissionRequestor methods def get_permission_actions(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT permission FROM private_report GROUP BY permission""") report_perms = [] try: for permission in cursor.fetchall(): report_perms.append(permission[0]) except: pass return tuple(report_perms) ### Internal methods def _has_permission(self, user, report_id): report_permissions = self._get_report_permissions(report_id) if not report_permissions: return True perms = PermissionSystem(self.env) report_permissions = set(report_permissions) user_perm = set(perms.get_user_permissions(user)) groups = set(self._get_user_groups(user)) user_perm.update(groups) if report_permissions.intersection(user_perm) != set([]): return True return False def _get_user_groups(self, user): subjects = set([user]) for provider in self.group_providers: subjects.update(provider.get_permission_groups(user) or []) groups = [] db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT action FROM permission WHERE username = %s""", (user,)) rows = cursor.fetchall() for action in rows: if action[0].isupper(): groups.append(action[0]) return groups def _get_reports(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT title, id FROM report") reports = cursor.fetchall() return reports def _insert_report_permission(self, report_id, permission): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" INSERT INTO private_report(report_id, permission) VALUES(%s, %s)""", (int(report_id), str(permission))) db.commit() def _alter_report_permissions(self, report_id, permissions): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" DELETE FROM private_report WHERE report_id=%s """, (int(report_id),)) db.commit() for permission in permissions: self._insert_report_permission(report_id, permission) def _get_report_permissions(self, report_id): report_perms = [] db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute(""" SELECT permission FROM private_report WHERE report_id=%s GROUP BY permission""", (int(report_id),)) for perm in cursor.fetchall(): report_perms.append(perm[0]) return report_perms
class SessionAdmin(Component): """trac-admin command provider for session management""" implements(IAdminCommandProvider) request_handlers = ExtensionPoint(IRequestHandler) def get_admin_commands(self): hints = { 'datetime': get_datetime_format_hint(get_console_locale(self.env)), 'iso8601': get_datetime_format_hint('iso8601'), } yield ('session list', '[sid[:0|1]] [...]', """List the name and email for the given sids Specifying the sid 'anonymous' lists all unauthenticated sessions, and 'authenticated' all authenticated sessions. '*' lists all sessions, and is the default if no sids are given. An sid suffix ':0' operates on an unauthenticated session with the given sid, and a suffix ':1' on an authenticated session (the default).""", self._complete_list, self._do_list) yield ('session add', '<sid[:0|1]> [name] [email]', """Create a session for the given sid Populates the name and email attributes for the given session. Adding a suffix ':0' to the sid makes the session unauthenticated, and a suffix ':1' makes it authenticated (the default if no suffix is specified).""", None, self._do_add) yield ('session set', '<name|email|default_handler> ' '<sid[:0|1]> <value>', """Set the name or email attribute of the given sid An sid suffix ':0' operates on an unauthenticated session with the given sid, and a suffix ':1' on an authenticated session (the default).""", self._complete_set, self._do_set) yield ('session delete', '<sid[:0|1]> [...]', """Delete the session of the specified sid An sid suffix ':0' operates on an unauthenticated session with the given sid, and a suffix ':1' on an authenticated session (the default). Specifying the sid 'anonymous' will delete all anonymous sessions.""", self._complete_delete, self._do_delete) yield ('session purge', '<age>', """Purge anonymous sessions older than the given age or date Age may be specified as a relative time like "90 days ago", or as a date in the "%(datetime)s" or "%(iso8601)s" (ISO 8601) format.""" % hints, None, self._do_purge) @lazy def _valid_default_handlers(self): return sorted(handler.__class__.__name__ for handler in self.request_handlers if is_valid_default_handler(handler)) def _split_sid(self, sid): if sid.endswith(':0'): return sid[:-2], 0 elif sid.endswith(':1'): return sid[:-2], 1 else: return sid, 1 def _get_sids(self): rows = self.env.db_query("SELECT sid, authenticated FROM session") return ['%s:%d' % (sid, auth) for sid, auth in rows] def _get_list(self, sids): all_anon = 'anonymous' in sids or '*' in sids all_auth = 'authenticated' in sids or '*' in sids sids = set( self._split_sid(sid) for sid in sids if sid not in ('anonymous', 'authenticated', '*')) rows = self.env.db_query(""" SELECT DISTINCT s.sid, s.authenticated, s.last_visit, n.value, e.value, h.value FROM session AS s LEFT JOIN session_attribute AS n ON (n.sid=s.sid AND n.authenticated=s.authenticated AND n.name='name') LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=s.authenticated AND e.name='email') LEFT JOIN session_attribute AS h ON (h.sid=s.sid AND h.authenticated=s.authenticated AND h.name='default_handler') ORDER BY s.sid, s.authenticated """) for sid, authenticated, last_visit, name, email, handler in rows: if all_anon and not authenticated or all_auth and authenticated \ or (sid, authenticated) in sids: yield (sid, authenticated, format_date(to_datetime(last_visit), console_date_format), name, email, handler) def _complete_list(self, args): all_sids = self._get_sids() + ['*', 'anonymous', 'authenticated'] return set(all_sids) - set(args) def _complete_set(self, args): if len(args) == 1: return ['name', 'email'] elif len(args) == 2: return self._get_sids() def _complete_delete(self, args): all_sids = self._get_sids() + ['anonymous'] return set(all_sids) - set(args) def _do_list(self, *sids): if not sids: sids = ['*'] headers = (_("SID"), _("Auth"), _("Last Visit"), _("Name"), _("Email"), _("Default Handler")) print_table(self._get_list(sids), headers) def _do_add(self, sid, name=None, email=None): sid, authenticated = self._split_sid(sid) with self.env.db_transaction as db: try: db("INSERT INTO session VALUES (%s, %s, %s)", (sid, authenticated, int(time.time()))) except Exception: raise AdminCommandError( _("Session '%(sid)s' already exists", sid=sid)) if name is not None: db("INSERT INTO session_attribute VALUES (%s,%s,'name',%s)", (sid, authenticated, name)) if email is not None: db("INSERT INTO session_attribute VALUES (%s,%s,'email',%s)", (sid, authenticated, email)) self.env.invalidate_known_users_cache() def _do_set(self, attr, sid, val): if attr not in ('name', 'email', 'default_handler'): raise AdminCommandError( _("Invalid attribute '%(attr)s'", attr=attr)) if attr == 'default_handler': if val and val not in self._valid_default_handlers: raise AdminCommandError( _("Invalid default_handler '%(val)s'", val=val)) sid, authenticated = self._split_sid(sid) with self.env.db_transaction as db: if not db( """SELECT sid FROM session WHERE sid=%s AND authenticated=%s""", (sid, authenticated)): raise AdminCommandError( _("Session '%(sid)s' not found", sid=sid)) db( """ DELETE FROM session_attribute WHERE sid=%s AND authenticated=%s AND name=%s """, (sid, authenticated, attr)) db("INSERT INTO session_attribute VALUES (%s, %s, %s, %s)", (sid, authenticated, attr, val)) self.env.invalidate_known_users_cache() def _do_delete(self, *sids): with self.env.db_transaction as db: for sid in sids: sid, authenticated = self._split_sid(sid) if sid == 'anonymous': db("DELETE FROM session WHERE authenticated=0") db("DELETE FROM session_attribute WHERE authenticated=0") else: db( """ DELETE FROM session WHERE sid=%s AND authenticated=%s """, (sid, authenticated)) db( """ DELETE FROM session_attribute WHERE sid=%s AND authenticated=%s """, (sid, authenticated)) self.env.invalidate_known_users_cache() def _do_purge(self, age): when = parse_date(age, hint='datetime', locale=get_console_locale(self.env)) with self.env.db_transaction as db: ts = to_timestamp(when) db( """ DELETE FROM session WHERE authenticated=0 AND last_visit<%s """, (ts, )) db(""" DELETE FROM session_attribute WHERE authenticated=0 AND sid NOT IN (SELECT sid FROM session WHERE authenticated=0) """)
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled acct_mgr.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `acct_mgr` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. """) base_url = Option( 'trac', 'base_url', '', """Reference URL for the Trac deployment. This is the base URL that will be used when producing documents that will be used outside of the web browsing context, like for example when inserting URLs pointing to Trac resources in notification e-mails.""") base_url_for_redirect = BoolOption( 'trac', 'use_base_url_for_redirect', False, """Optionally use `[trac] base_url` for redirects. In some configurations, usually involving running Trac behind a HTTP proxy, Trac can't automatically reconstruct the URL that is used to access it. You may need to use this option to force Trac to use the `base_url` setting also for redirects. This introduces the obvious limitation that this environment will only be usable when accessible from that URL, as redirects are frequently used. """) secure_cookies = BoolOption( 'trac', 'secure_cookies', False, """Restrict cookies to HTTPS connections. When true, set the `secure` flag on all cookies so that they are only sent to the server on HTTPS connections. Use this if your Trac instance is only accessible through HTTPS. """) project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option( 'project', 'url', '', """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. """) project_footer = Option( 'project', 'footer', N_('Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>'), """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = ChoiceOption('logging', 'log_type', log.LOG_TYPES + log.LOG_TYPE_ALIASES, """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""", case_sensitive=False) log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = ChoiceOption('logging', 'log_level', tuple(reversed(log.LOG_LEVELS)) + log.LOG_LEVEL_ALIASES, """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`). """, case_sensitive=False) log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: `Trac[$(module)s] $(levelname)s: $(message)s` In addition to regular key names supported by the [http://docs.python.org/library/logging.html Python logger library] one could use: - `$(path)s` the path for the current environment - `$(basename)s` the last path component of the current environment - `$(project)s` the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the !ConfigParser itself. Example: `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` """) def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. :param path: the absolute path to the Trac environment :param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. :param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = os.path.normpath(os.path.normcase(path)) self.log = None self.config = None if create: self.create(options) for setup_participant in self.setup_participants: setup_participant.environment_created() else: self.verify() self.setup_config() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.path) @lazy def name(self): """The environment name. :since: 1.2 """ return os.path.basename(self.path) @property def env(self): """Property returning the `Environment` object, which is often required for functions and methods that take a `Component` instance. """ # The cached decorator requires the object have an `env` attribute. return self @property def system_info(self): """List of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = [] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) return sorted(set(info), key=lambda args: (args[0] != 'Trac', args[0].lower())) def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. :since 1.3.1: deprecated and will be removed in 1.5.1. Use system_info property instead. """ return self.system_info # ISystemInfoProvider methods def get_system_info(self): yield 'Trac', self.trac_version yield 'Python', sys.version yield 'setuptools', setuptools.__version__ if pytz is not None: yield 'pytz', pytz.__version__ if hasattr(self, 'webfrontend_version'): yield self.webfrontend, self.webfrontend_version def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @lazy def _component_rules(self): _rules = {} for name, value in self.components_section.options(): name = name.rstrip('.*').lower() _rules[name] = as_bool(value) return _rules def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns `False`, the component does not get activated. If it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package except # in trac.test or trac.tests are enabled return component_name.startswith('trac.') and \ not component_name.startswith('trac.test.') and \ not component_name.startswith('trac.tests.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True super(Environment, self).enable_component(cls) @contextmanager def component_guard(self, component, reraise=False): """Traps any runtime exception raised when working with a component and logs the error. :param component: the component responsible for any error that could happen inside the context :param reraise: if `True`, an error is logged but not suppressed. By default, errors are suppressed. """ try: yield except TracError as e: self.log.warning("Component %s failed with %s", component, exception_to_unicode(e)) if reraise: raise except Exception as e: self.log.error("Component %s failed with %s", component, exception_to_unicode(e, traceback=True)) if reraise: raise def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" try: tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] if tag != _VERSION: raise Exception( _("Unknown Trac environment type '%(type)s'", type=tag)) except Exception as e: raise TracError( _("No Trac environment found at %(path)s\n" "%(e)s", path=self.path, e=e)) @lazy def db_exc(self): """Return an object (typically a module) containing all the backend-specific exception types as attributes, named according to the Python Database API (http://www.python.org/dev/peps/pep-0249/). To catch a database exception, use the following pattern:: try: with env.db_transaction as db: ... except env.db_exc.IntegrityError as e: ... """ return DatabaseManager(self).get_exceptions() @property def db_query(self): """Return a context manager (`~trac.db.api.QueryContextManager`) which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still defined, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager (`~trac.db.api.TransactionContextManager`) which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. You should *not* call `commit()` yourself within such block, as this will force a commit even if that transaction is part of a larger transaction. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" from trac.versioncontrol.api import RepositoryManager RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: log.shutdown(self.log) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values. If options contains ('inherit', 'file'), default values will not be loaded; they are expected to be provided by that file or other options. :raises TracError: if the base directory of `path` does not exist. :raises TracError: if `path` exists and is not empty. """ base_dir = os.path.dirname(self.path) if not os.path.exists(base_dir): raise TracError( _( "Base directory '%(env)s' does not exist. Please create it " "and retry.", env=base_dir)) if os.path.exists(self.path) and os.listdir(self.path): raise TracError(_("Directory exists and is not empty.")) # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.htdocs_dir) os.mkdir(self.log_dir) os.mkdir(self.plugins_dir) os.mkdir(self.templates_dir) # Create a few files create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n') create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(self.conf_dir) config = Configuration(self.config_file_path) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the sample configuration create_file(self.config_file_path + '.sample') self._update_sample_config() # Create the database DatabaseManager(self).init_db() @lazy def database_version(self): """Returns the current version of the database. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('database_version') @lazy def database_initial_version(self): """Returns the version of the database at the time of creation. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('initial_database_version') @lazy def trac_version(self): """Returns the version of Trac. :since: 1.2 """ from trac import core, __version__ return get_pkginfo(core).get('version', __version__) def setup_config(self): """Load the configuration file.""" self.config = Configuration(self.config_file_path, {'envname': self.name}) if not self.config.exists: raise TracError( _("The configuration file is not found at " "%(path)s", path=self.config_file_path)) self.setup_log() plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) @lazy def config_file_path(self): """Path of the trac.ini file.""" return os.path.join(self.conf_dir, 'trac.ini') @lazy def log_file_path(self): """Path to the log file.""" if not os.path.isabs(self.log_file): return os.path.join(self.log_dir, self.log_file) return self.log_file def _get_path_to_dir(self, *dirs): path = self.path for dir in dirs: path = os.path.join(path, dir) return os.path.realpath(path) @lazy def attachments_dir(self): """Absolute path to the attachments directory. :since: 1.3.1 """ return self._get_path_to_dir('files', 'attachments') @lazy def conf_dir(self): """Absolute path to the conf directory. :since: 1.0.11 """ return self._get_path_to_dir('conf') @lazy def files_dir(self): """Absolute path to the files directory. :since: 1.3.2 """ return self._get_path_to_dir('files') @lazy def htdocs_dir(self): """Absolute path to the htdocs directory. :since: 1.0.11 """ return self._get_path_to_dir('htdocs') @lazy def log_dir(self): """Absolute path to the log directory. :since: 1.0.11 """ return self._get_path_to_dir('log') @lazy def plugins_dir(self): """Absolute path to the plugins directory. :since: 1.0.11 """ return self._get_path_to_dir('plugins') @lazy def templates_dir(self): """Absolute path to the templates directory. :since: 1.0.11 """ return self._get_path_to_dir('templates') def setup_log(self): """Initialize the logging sub-system.""" self.log, log_handler = \ self.create_logger(self.log_type, self.log_file_path, self.log_level, self.log_format) self.log.addHandler(log_handler) self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, self.trac_version) def create_logger(self, log_type, log_file, log_level, log_format): log_id = 'Trac.%s' % hashlib.sha1(self.path).hexdigest() if log_format: log_format = log_format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', self.name) \ .replace('%(project)s', self.project_name) return log.logger_handler_factory(log_type, log_file, log_level, log_id, format=log_format) def get_known_users(self, as_dict=False): """Returns information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. By default this function returns a iterator that yields one tuple for every user, of the form (username, name, email), ordered alpha-numerically by username. When `as_dict` is `True` the function returns a dictionary mapping username to a (name, email) tuple. :since 1.2: the `as_dict` parameter is available. """ return self._known_users_dict if as_dict else iter(self._known_users) @cached def _known_users(self): return self.db_query(""" SELECT DISTINCT s.sid, n.value, e.value FROM session AS s LEFT JOIN session_attribute AS n ON (n.sid=s.sid AND n.authenticated=1 AND n.name = 'name') LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=1 AND e.name = 'email') WHERE s.authenticated=1 ORDER BY s.sid """) @cached def _known_users_dict(self): return {u[0]: (u[1], u[2]) for u in self._known_users} def invalidate_known_users_cache(self): """Clear the known_users cache.""" del self._known_users del self._known_users_dict def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): self.log.warning( "Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): upgraders.append(participant) if not upgraders: return if backup: try: self.backup(backup_dest) except Exception as e: raise BackupError(e) for participant in upgraders: self.log.info("upgrading %s...", participant) with self.component_guard(participant, reraise=True): participant.upgrade_environment() # Database schema may have changed, so close all connections dbm = DatabaseManager(self) if dbm.connection_uri != 'sqlite::memory:': dbm.shutdown() self._update_sample_config() del self.database_version return True @lazy def href(self): """The application root path""" return Href(urlsplit(self.abs_href.base).path) @lazy def abs_href(self): """The application URL""" if not self.base_url: self.log.warning("base_url option not set in configuration, " "generated links may be incorrect") return Href(self.base_url) def _update_sample_config(self): filename = os.path.join(self.config_file_path + '.sample') if not os.path.isfile(filename): return config = Configuration(filename) config.set_defaults() try: config.save() except IOError as e: self.log.warning("Couldn't write sample configuration file (%s)%s", e, exception_to_unicode(e, traceback=True)) else: self.log.info( "Wrote sample configuration file with the new " "settings and their default values: %s", filename)
class TimelineWidget(WidgetBase): """Display activity feed. """ default_count = IntOption( 'widget_activity', 'limit', 25, """Maximum number of items displayed by default""") event_filters = ExtensionPoint(ITimelineEventsFilter) _filters_map = None @property def filters_map(self): """Quick access to timeline events filters to be applied for a given timeline provider. """ if self._filters_map is None: self._filters_map = {} for _filter in self.event_filters: providers = _filter.supported_providers() if providers is None: providers = [None] for p in providers: self._filters_map.setdefault(p, []).append(_filter) return self._filters_map def get_widget_params(self, name): """Return a dictionary containing arguments specification for the widget with specified name. """ return { 'from': { 'desc': """Display events before this date""", 'type': DateField(), # TODO: Custom datetime format }, 'daysback': { 'desc': """Event time window""", 'type': int, }, 'precision': { 'desc': """Time precision""", 'type': EnumField('second', 'minute', 'hour') }, 'doneby': { 'desc': """Filter events related to user""", }, 'filters': { 'desc': """Event filters""", 'type': ListField() }, 'max': { 'desc': """Limit the number of events displayed""", 'type': int }, 'realm': { 'desc': """Resource realm. Used to filter events""", }, 'id': { 'desc': """Resource ID. Used to filter events""", }, } get_widget_params = pretty_wrapper(get_widget_params, check_widget_name) def render_widget(self, name, context, options): """Gather timeline events and render data in compact view """ data = None req = context.req try: timemdl = self.env[TimelineModule] admin_page = tag.a(_("administration page."), title=_("Plugin Administration Page"), href=req.href.admin('general/plugin')) if timemdl is None: return 'widget_alert.html', { 'title': _("Activity"), 'data': { 'msglabel': "Warning", 'msgbody': tag_( "The TimelineWidget is disabled because the " "Timeline component is not available. " "Is the component disabled? " "You can enable from the %(page)s", page=admin_page), 'dismiss': False, } }, context params = ('from', 'daysback', 'doneby', 'precision', 'filters', 'max', 'realm', 'id') start, days, user, precision, filters, count, realm, rid = \ self.bind_params(name, options, *params) if context.resource.realm == 'ticket': if days is None: # calculate a long enough time daysback ticket = Ticket(self.env, context.resource.id) ticket_age = datetime.now(utc) - ticket.time_created days = ticket_age.days + 1 if count is None: # ignore short count for ticket feeds count = 0 if count is None: count = self.default_count fakereq = dummy_request(self.env, req.authname) fakereq.args = { 'author': user or '', 'daysback': days or '', 'max': count, 'precision': precision, 'user': user } if filters: fakereq.args.update(dict((k, True) for k in filters)) if start is not None: fakereq.args['from'] = start.strftime('%x %X') wcontext = context.child() if (realm, rid) != (None, None): # Override rendering context resource = Resource(realm, rid) if resource_exists(self.env, resource) or \ realm == rid == '': wcontext = context.child(resource) wcontext.req = req else: self.log.warning("TimelineWidget: Resource %s not found", resource) # FIXME: Filter also if existence check is not conclusive ? if resource_exists(self.env, wcontext.resource): module = FilteredTimeline(self.env, wcontext) self.log.debug('Filtering timeline events for %s', wcontext.resource) else: module = timemdl data = module.process_request(fakereq)[1] except TracError, exc: if data is not None: exc.title = data.get('title', 'Activity') raise else:
class Mimeview(Component): """Generic HTML renderer for data, typically source code.""" required = True renderers = ExtensionPoint(IHTMLPreviewRenderer) annotators = ExtensionPoint(IHTMLPreviewAnnotator) converters = ExtensionPoint(IContentConverter) default_charset = Option('trac', 'default_charset', 'utf-8', """Charset to be used when in doubt.""") tab_width = IntOption('mimeviewer', 'tab_width', 8, """Displayed tab width in file preview.""") max_preview_size = IntOption('mimeviewer', 'max_preview_size', 262144, """Maximum file size for HTML preview.""") mime_map = ListOption( 'mimeviewer', 'mime_map', 'text/x-dylan:dylan, text/x-idl:ice, text/x-ada:ads:adb', doc="""List of additional MIME types and keyword mappings. Mappings are comma-separated, and for each MIME type, there's a colon (":") separated list of associated keywords or file extensions. """) mime_map_patterns = ListOption( 'mimeviewer', 'mime_map_patterns', 'text/plain:README(?!\.rst)|INSTALL(?!\.rst)|COPYING.*', doc="""List of additional MIME types associated to filename patterns. Mappings are comma-separated, and each mapping consists of a MIME type and a Python regexp used for matching filenames, separated by a colon (":"). (''since 1.0'') """) treat_as_binary = ListOption( 'mimeviewer', 'treat_as_binary', 'application/octet-stream, application/pdf, application/postscript, ' 'application/msword, application/rtf', doc="""Comma-separated list of MIME types that should be treated as binary data. """) def __init__(self): self._mime_map = None self._mime_map_patterns = None # Public API def get_supported_conversions(self, mimetype): """Return a list of target MIME types as instances of the `namedtuple` `MimeConversion`. Output is ordered from best to worst quality. The `MimeConversion` `namedtuple` has fields: key, name, extension, in_mimetype, out_mimetype, quality, converter. """ fields = ('key', 'name', 'extension', 'in_mimetype', 'out_mimetype', 'quality', 'converter') _MimeConversion = namedtuple('MimeConversion', fields) converters = [] for c in self.converters: for k, n, e, im, om, q in c.get_supported_conversions() or []: if im == mimetype and q > 0: converters.append(_MimeConversion(k, n, e, im, om, q, c)) converters = sorted(converters, key=lambda i: i.quality, reverse=True) return converters def convert_content(self, req, mimetype, content, key, filename=None, url=None, iterable=False): """Convert the given content to the target MIME type represented by `key`, which can be either a MIME type or a key. Returns a tuple of (content, output_mime_type, extension).""" if not content: return '', 'text/plain;charset=utf-8', '.txt' # Ensure we have a MIME type for this content full_mimetype = mimetype if not full_mimetype: if hasattr(content, 'read'): content = content.read(self.max_preview_size) full_mimetype = self.get_mimetype(filename, content) if full_mimetype: mimetype = ct_mimetype(full_mimetype) # split off charset else: mimetype = full_mimetype = 'text/plain' # fallback if not binary # Choose best converter candidates = [ c for c in self.get_supported_conversions(mimetype) if key in (c.key, c.out_mimetype) ] if not candidates: raise TracError( _("No available MIME conversions from %(old)s to %(new)s", old=mimetype, new=key)) # First successful conversion wins for conversion in candidates: output = conversion.converter.convert_content( req, mimetype, content, conversion.key) if output: content, content_type = output if iterable: if isinstance(content, basestring): content = (content, ) else: if not isinstance(content, basestring): content = ''.join(content) return content, content_type, conversion.extension raise TracError( _("No available MIME conversions from %(old)s to %(new)s", old=mimetype, new=key)) def get_annotation_types(self): """Generator that returns all available annotation types.""" for annotator in self.annotators: yield annotator.get_annotation_type() def render(self, context, mimetype, content, filename=None, url=None, annotations=None, force_source=False): """Render an XHTML preview of the given `content`. `content` is the same as an `IHTMLPreviewRenderer.render`'s `content` argument. The specified `mimetype` will be used to select the most appropriate `IHTMLPreviewRenderer` implementation available for this MIME type. If not given, the MIME type will be infered from the filename or the content. Return a string containing the XHTML text. When rendering with an `IHTMLPreviewRenderer` fails, a warning is added to the request associated with the context (if any), unless the `disable_warnings` hint is set to `True`. """ if not content: return '' # Ensure we have a MIME type for this content full_mimetype = mimetype if not full_mimetype: if hasattr(content, 'read'): content = content.read(self.max_preview_size) full_mimetype = self.get_mimetype(filename, content) if full_mimetype: mimetype = ct_mimetype(full_mimetype) # split off charset else: mimetype = full_mimetype = 'text/plain' # fallback if not binary # Determine candidate `IHTMLPreviewRenderer`s candidates = [] for renderer in self.renderers: qr = renderer.get_quality_ratio(mimetype) if qr > 0: candidates.append((qr, renderer)) candidates.sort(key=lambda item: -item[0]) # Wrap file-like object so that it can be read multiple times if hasattr(content, 'read'): content = Content(content, self.max_preview_size) # First candidate which renders successfully wins. # Also, we don't want to expand tabs more than once. expanded_content = None for qr, renderer in candidates: if force_source and not getattr(renderer, 'returns_source', False): continue # skip non-source renderers in force_source mode if isinstance(content, Content): content.reset() ann_names = ', '.join(annotations) if annotations else \ 'no annotations' self.log.debug('Trying to render HTML preview using %s [%s]', renderer.__class__.__name__, ann_names) # check if we need to perform a tab expansion rendered_content = content if getattr(renderer, 'expand_tabs', False): if expanded_content is None: content = content_to_unicode(self.env, content, full_mimetype) expanded_content = content.expandtabs(self.tab_width) rendered_content = expanded_content try: result = renderer.render(context, full_mimetype, rendered_content, filename, url) except Exception as e: self.log.warning('HTML preview using %s with %r failed: %s', renderer.__class__.__name__, context, exception_to_unicode(e, traceback=True)) if context.req and not context.get_hint('disable_warnings'): from trac.web.chrome import add_warning add_warning( context.req, _("HTML preview using %(renderer)s failed (%(err)s)", renderer=renderer.__class__.__name__, err=exception_to_unicode(e))) else: if not result: continue if not (force_source or getattr(renderer, 'returns_source', False)): # Direct rendering of content if isinstance(result, basestring): return Markup(to_unicode(result)) elif isinstance(result, Fragment): return result.__html__() else: return result # Render content as source code if annotations: marks = context.req.args.get('marks') if context.req \ else None if marks: context.set_hints(marks=marks) return self._render_source(context, result, annotations) else: if isinstance(result, list): result = Markup('\n').join(result) return tag.div(class_='code')(tag.pre(result)) def _render_source(self, context, lines, annotations): from trac.web.chrome import add_warning annotators, labels, titles = {}, {}, {} for annotator in self.annotators: atype, alabel, atitle = annotator.get_annotation_type() if atype in annotations: labels[atype] = alabel titles[atype] = atitle annotators[atype] = annotator annotations = [a for a in annotations if a in annotators] if isinstance(lines, unicode): lines = lines.splitlines(True) # elif isinstance(lines, list): # pass # assume these are lines already annotator_datas = [] for a in annotations: annotator = annotators[a] try: data = (annotator, annotator.get_annotation_data(context)) except TracError as e: self.log.warning("Can't use annotator '%s': %s", a, e) add_warning( context.req, tag.strong( tag_("Can't use %(annotator)s annotator: %(error)s", annotator=tag.em(a), error=tag.pre(e)))) data = None, None annotator_datas.append(data) def _head_row(): return tag.tr([ tag.th(labels[a], class_=a, title=titles[a]) for a in annotations ] + [tag.th(u'\xa0', class_='content')]) def _body_rows(): for idx, line in enumerate(lines): row = tag.tr() for annotator, data in annotator_datas: if annotator: annotator.annotate_row(context, row, idx + 1, line, data) else: row.append(tag.td()) row.append(tag.td(line)) yield row return tag.table(class_='code')(tag.thead(_head_row()), tag.tbody(_body_rows())) def get_charset(self, content='', mimetype=None): """Infer the character encoding from the `content` or the `mimetype`. `content` is either a `str` or an `unicode` object. The charset will be determined using this order: * from the charset information present in the `mimetype` argument * auto-detection of the charset from the `content` * the configured `default_charset` """ if mimetype: ctpos = mimetype.find('charset=') if ctpos >= 0: return mimetype[ctpos + 8:].strip() if isinstance(content, str): utf = detect_unicode(content) if utf is not None: return utf return self.default_charset @property def mime_map(self): # Extend default extension to MIME type mappings with configured ones if not self._mime_map: self._mime_map = MIME_MAP.copy() # augment mime_map from `IHTMLPreviewRenderer`s for renderer in self.renderers: if hasattr(renderer, 'get_extra_mimetypes'): for mimetype, kwds in renderer.get_extra_mimetypes() or []: self._mime_map[mimetype] = mimetype for keyword in kwds: self._mime_map[keyword] = mimetype # augment/override mime_map from trac.ini for mapping in self.config['mimeviewer'].getlist('mime_map'): if ':' in mapping: assocations = mapping.split(':') for keyword in assocations: # Note: [0] kept on purpose self._mime_map[keyword] = assocations[0] return self._mime_map def get_mimetype(self, filename, content=None): """Infer the MIME type from the `filename` or the `content`. `content` is either a `str` or an `unicode` object. Return the detected MIME type, augmented by the charset information (i.e. "<mimetype>; charset=..."), or `None` if detection failed. """ mimetype = get_mimetype(filename, content, self.mime_map, self.mime_map_patterns) charset = None if mimetype: charset = self.get_charset(content, mimetype) if mimetype and charset and 'charset' not in mimetype: mimetype += '; charset=' + charset return mimetype @property def mime_map_patterns(self): if not self._mime_map_patterns: self._mime_map_patterns = {} for mapping in self.config['mimeviewer'] \ .getlist('mime_map_patterns'): if ':' in mapping: mimetype, regexp = mapping.split(':', 1) try: self._mime_map_patterns[mimetype] = re.compile(regexp) except re.error as e: self.log.warning( "mime_map_patterns contains invalid " "regexp '%s' for mimetype '%s' (%s)", regexp, mimetype, exception_to_unicode(e)) return self._mime_map_patterns def is_binary(self, mimetype=None, filename=None, content=None): """Check if a file must be considered as binary.""" if not mimetype and filename: mimetype = self.get_mimetype(filename, content) if mimetype: mimetype = ct_mimetype(mimetype) if mimetype in self.treat_as_binary: return True if content is not None and is_binary(content): return True return False def to_unicode(self, content, mimetype=None, charset=None): """Convert `content` (an encoded `str` object) to an `unicode` object. This calls `trac.util.to_unicode` with the `charset` provided, or the one obtained by `Mimeview.get_charset()`. """ if not charset: charset = self.get_charset(content, mimetype) return to_unicode(content, charset) def configured_modes_mapping(self, renderer): """Return a MIME type to `(mode,quality)` mapping for given `option`""" types, option = {}, '%s_modes' % renderer for mapping in self.config['mimeviewer'].getlist(option): if not mapping: continue try: mimetype, mode, quality = mapping.split(':') types[mimetype] = (mode, int(quality)) except (TypeError, ValueError): self.log.warning( "Invalid mapping '%s' specified in '%s' " "option.", mapping, option) return types def preview_data(self, context, content, length, mimetype, filename, url=None, annotations=None, force_source=False): """Prepares a rendered preview of the given `content`. Note: `content` will usually be an object with a `read` method. """ data = { 'raw_href': url, 'size': length, 'max_file_size': self.max_preview_size, 'max_file_size_reached': False, 'rendered': None, } if length >= self.max_preview_size: data['max_file_size_reached'] = True else: result = self.render(context, mimetype, content, filename, url, annotations, force_source=force_source) data['rendered'] = result return data def send_converted(self, req, in_type, content, selector, filename='file'): """Helper method for converting `content` and sending it directly. `selector` can be either a key or a MIME Type.""" from trac.web.chrome import Chrome from trac.web.api import RequestDone iterable = Chrome(self.env).use_chunked_encoding content, output_type, ext = self.convert_content(req, in_type, content, selector, iterable=iterable) if iterable: def encoder(content): for chunk in content: if isinstance(chunk, unicode): chunk = chunk.encode('utf-8') yield chunk content = encoder(content) length = None else: if isinstance(content, unicode): content = content.encode('utf-8') length = len(content) req.send_response(200) req.send_header('Content-Type', output_type) if length is not None: req.send_header('Content-Length', length) if filename: req.send_header( 'Content-Disposition', content_disposition('attachment', '%s.%s' % (filename, ext))) req.end_headers() req.write(content) raise RequestDone
class AgiloInit(PluginEnvironmentSetup): """ Initialize database and environment for link component """ setup_listeners = ExtensionPoint(IAgiloEnvironmentSetupListener) # PluginEnvironmentSetup template methods def get_db_version(self, db): """Returns the normalized db version (integer). This method can convert the decimal numbers from Agilo 0.6 to integer.""" fetch_version = super(AgiloInit, self)._fetch_db_version db_version = fetch_version(db, self.name) if db_version == 0: # Agilo versions before 0.7 had different database versions with # floating point old_version = fetch_version(db, name='agilo-types') if old_version == '1.2': db_version = 1 elif old_version != 0: msg = _('Unknown version for agilo-types: %s') % old_version raise TracError(msg) elif db_version == '0.7': db_version = 2 # 'Modern' Agilo versions like 0.7 just return something like '3'. db_version = int(db_version) return db_version def get_package_name(self): return 'agilo.db.upgrades' def get_expected_db_version(self): return db_default.db_version def name(self): # Do not modify this because it is used in the DB! return 'agilo' name = property(name) def set_db_version(self, db): cursor = db.cursor() latest_version = self.get_expected_db_version() # If there is an update from 0.6 -> 0.7 we have a version number 1 but # 'agilo' does not exist in the DB. was_upgrade = (self.get_db_version(db) > 1) if was_upgrade: self._update_version_number(cursor, latest_version) else: self._insert_version_number(cursor, latest_version) #========================================================================== # IEnvironmentSetupParticipant #========================================================================== def environment_created(self): for table in db_default.schema: create_table(self.env, table) cache_manager = HttpRequestCacheManager(self.env) po_manager = PersistentObjectManager(self.env) for manager in cache_manager.managers: model_class = manager.for_model() if issubclass(model_class, PersistentObject): module_name = model_class.__module__ # We don't want to create tables for dummy classes automatically # but the test finder may load some of these managers so we # need to exclude them here. if ('tests.' not in module_name): po_manager.create_table(model_class) # Need to create Agilo types in the database before writing to the # configuration - otherwise we get a warning during config setup (you'll # see it in every test case during setUp) db_default.create_default_types(self.env) initialize_config(self.env, __CONFIG_PROPERTIES__) db_default.create_default_backlogs(self.env) super(AgiloInit, self).environment_created() for listener in self.setup_listeners: listener.agilo_was_installed() # Reload the AgiloConfig to make sure all the changes have been updated AgiloConfig(self.env).reload() info(self, 'Agilo environment initialized')
class BloodhoundSearchApi(Component): """Implements core indexing functionality, provides methods for searching, adding and deleting documents from index. """ implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment) backend = ExtensionOption( 'bhsearch', 'search_backend', ISearchBackend, 'WhooshBackend', 'Name of the component implementing Bloodhound Search backend \ interface: ISearchBackend.') parser = ExtensionOption( 'bhsearch', 'query_parser', IQueryParser, 'DefaultQueryParser', 'Name of the component implementing Bloodhound Search query \ parser.') index_pre_processors = OrderedExtensionsOption( 'bhsearch', 'index_preprocessors', IDocIndexPreprocessor, ['SecurityPreprocessor'], ) result_post_processors = ExtensionPoint(IResultPostprocessor) query_processors = ExtensionPoint(IQueryPreprocessor) index_participants = MultiProductExtensionPoint(IIndexParticipant) def query(self, query, sort=None, fields=None, filter=None, facets=None, pagenum=1, pagelen=20, highlight=False, highlight_fields=None, context=None): """Return query result from an underlying search backend. Arguments: :param query: query string e.g. “bla status:closed” or a parsed representation of the query. :param sort: optional sorting :param boost: optional list of fields with boost values e.g. {“id”: 1000, “subject” :100, “description”:10}. :param filter: optional list of terms. Usually can be cached by underlying search framework. For example {“type”: “wiki”} :param facets: optional list of facet terms, can be field or expression :param page: paging support :param pagelen: paging support :param highlight: highlight matched terms in fields :param highlight_fields: list of fields to highlight :param context: request context :return: result QueryResult """ # pylint: disable=too-many-locals self.env.log.debug("Receive query request: %s", locals()) parsed_query = self.parser.parse(query, context) parsed_filters = self.parser.parse_filters(filter) # TODO: add query parsers and meta keywords post-parsing # TODO: apply security filters query_parameters = dict( query=parsed_query, query_string=query, sort=sort, fields=fields, filter=parsed_filters, facets=facets, pagenum=pagenum, pagelen=pagelen, highlight=highlight, highlight_fields=highlight_fields, ) for query_processor in self.query_processors: query_processor.query_pre_process(query_parameters, context) query_result = self.backend.query(**query_parameters) for post_processor in self.result_post_processors: post_processor.post_process(query_result) query_result.debug["api_parameters"] = query_parameters return query_result def start_operation(self): return self.backend.start_operation() def rebuild_index(self): """Rebuild underlying index""" self.log.info('Rebuilding the search index.') self.backend.recreate_index() with self.backend.start_operation() as operation_context: doc = None try: for participant in self.index_participants: self.log.info( "Reindexing resources provided by %s in product %s" % (participant.__class__.__name__, getattr(participant.env.product, 'name', "''"))) docs = participant.get_entries_for_index() for doc in docs: self.log.debug("Indexing document %s:%s/%s" % ( doc['product'], doc['type'], doc['id'], )) self.add_doc(doc, operation_context) self.log.info("Reindexing complete.") except Exception, ex: self.log.error(ex) if doc: self.log.error("Doc that triggers the error: %s" % doc) raise
class UserProfileBox(Component): implements(IRequestHandler, ITemplateProvider, IUserProfileActions, IRequestFilter) profile_action_providers = ExtensionPoint(IUserProfileActions) # IRequestHandler methods def match_request(self, req): return req.path_info.endswith('/profilebox') def process_request(self, req): """ Handles the profile box request, which is expected to be in format:: /user/<username>/profilebox :returns: Pre-rendered user profile box HTML using templates: - multiproject_user_profilebox.html: To show the content - multiproject_user_profilebox_default.html: In a case of failures, missing data etc """ # Read and validate arguments #account_id = req.args.get('user_id', None) #account_username = req.args.get('username', None) match = MATCHREGX.match(req.path_info) if not match: msg = _('Account cannot be found') return 'multiproject_user_profilebox_default.html', { 'msg': msg }, False # Load specified account userstore = get_userstore() account = userstore.getUser(match.group('username')) if not account: msg = _('Account cannot be found') return 'multiproject_user_profilebox_default.html', { 'msg': msg }, False # Check if user has USER_VIEW permission to view other users in home project homeperm = self._get_home_perm(req) if req.authname != account.username and 'USER_VIEW' not in homeperm: msg = _('Access denied') return 'multiproject_user_profilebox_default.html', { 'msg': msg }, False # Load registered actions actions = [] for aprovider in self.profile_action_providers: # Iterate actions and validate them for new_action in aprovider.get_profile_actions(req, account): # Ensure each action is in tuple format: (order, fragment) if not isinstance(new_action, tuple): new_action = (0, new_action) # Add to list actions.append(new_action) # Sort the actions by hints given in actions: # the smaller the value, the higher the priority actions.sort(key=lambda tup: tup[0]) # Construct list items: put first and class values litems = [] llen = len(actions) for index, action in enumerate(actions): classes = [] # If last if index == 0: classes.append('first') if llen == index + 1: classes.append('last') litems.append(tag.li(action[1], **{'class': ' '.join(classes)})) # If empty, put empty list element in place for easier styling if not litems: litems.append(tag.li('li', **{'class': 'first last'})) # Pass data to template. Generate ul/li list from registered actions data = {'account': account, 'actionlist': tag.ul(*litems)} return 'multiproject_user_profilebox.html', data, False # ITemplateProvider methods def get_htdocs_dirs(self): return [('multiproject', pkg_resources.resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return [ pkg_resources.resource_filename('multiproject.common.users', 'templates') ] # IUserProfileActions def get_profile_actions(self, req, user): """ Return list of actions """ actions = [] homeperm = self._get_home_perm(req) uresource = Resource('user', user.id) # Project settings for own account if user.username == req.authname: actions.append((-40, tag.a(_('View your projects'), href=homeperm.env.href('myprojects')))) actions.append((0, tag.a(_('Edit your settings'), href=homeperm.env.href('prefs')))) # View other user profile else: actions.append( (-50, tag.a(_('View service profile'), href=homeperm.env.href('user', user.username)))) # If user can fully manage account if homeperm.has_permission('USER_EDIT', uresource): label = 'Manage your account' if user.username == req.authname else 'Manage account' actions.append( (-2, tag.a(_(label), href=homeperm.env.href('admin/users/manage', username=user.username)))) # Tickets assigned to or created by user (if component is enabled and user has permissions to view them) # Note: href.kwargs cannot be used because of reserved word 'or' and un-ordered nature of dict if (self.env.is_component_enabled('trac.ticket.query.QueryModule') or self.env.is_component_enabled('multiproject.project.tickets.viewtickets.QueryModuleInterceptor')) \ and 'TICKET_VIEW' in req.perm: qstring = 'owner={0}&or&reporter={0}&group=status'.format( user.username) query = Query.from_string(self.env, qstring) actions.append((5, (tag.a(_('View tickets'), href=query.get_href(req.href))))) return actions def _get_home_perm(self, req): """ Returns permission cache from home environment """ home_env = open_environment(os.path.join( self.env.config.get('multiproject', 'sys_projects_root'), self.env.config.get('multiproject', 'sys_home_project_name')), use_cache=True) return PermissionCache(home_env, req.authname) # IRequestFilter methods def pre_process_request(self, req, handler): """ Process request to add some data in request """ return handler def post_process_request(self, req, template, data, content_type): """ Add global javascript data on post-processing phase """ # When processing template, add global javascript json into scripts if template: add_stylesheet(req, 'multiproject/css/multiproject.css') add_script(req, 'multiproject/js/multiproject.js') add_script(req, 'multiproject/js/profile.js') add_script(req, 'multiproject/js/preference.js') return template, data, content_type
class Environment(Component, ComponentManager): """Trac stores project information in a Trac environment. A Trac environment consists of a directory structure containing among other things: * a configuration file. * an SQLite database (stores tickets, wiki pages...) * Project specific templates and wiki macros. * wiki and ticket attachments. """ setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) base_url = Option( 'trac', 'base_url', '', """Base URL of the Trac deployment. In most configurations, Trac will automatically reconstruct the URL that is used to access it automatically. However, in more complex setups, usually involving running Trac behind a HTTP proxy, you may need to use this option to force Trac to use the correct URL.""") project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option('project', 'url', 'http://example.org/', """URL of the main project web site.""") project_footer = Option( 'project', 'footer', 'Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>', """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = Option( 'logging', 'log_type', 'none', """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""") log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file.""") log_level = Option( 'logging', 'log_level', 'DEBUG', """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""") log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: Trac[$(module)s] $(levelname)s: $(message)s In addition to regular key names supported by the Python logger library library (see http://docs.python.org/lib/node422.html), one could use: - $(path)s the path for the current environment - $(basename)s the last path component of the current environment - $(project)s the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the ConfigParser itself. Example: ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s (since 0.11)""") def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. @param path: the absolute path to the Trac environment @param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. @param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = path self.setup_config(load_defaults=create) self.setup_log() from trac.loader import load_components load_components(self) if create: self.create(options) else: self.verify() if create: for setup_participant in self.setup_participants: setup_participant.environment_created() def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns false, the component does not get activated.""" if not isinstance(cls, basestring): component_name = (cls.__module__ + '.' + cls.__name__).lower() else: component_name = cls.lower() rules = [(name.lower(), value.lower() in ('enabled', 'on')) for name, value in self.config.options('components')] rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0]))) for pattern, enabled in rules: if component_name == pattern or pattern.endswith('*') \ and component_name.startswith(pattern[:-1]): return enabled # versioncontrol components are enabled if the repository is configured # FIXME: this shouldn't be hardcoded like this if component_name.startswith('trac.versioncontrol.'): return self.config.get('trac', 'repository_dir') != '' # By default, all components in the trac package are enabled return component_name.startswith('trac.') def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" fd = open(os.path.join(self.path, 'VERSION'), 'r') try: assert fd.read(26) == 'Trac Environment Version 1' finally: fd.close() def get_db_cnx(self): """Return a database connection from the connection pool.""" return DatabaseManager(self).get_connection() def shutdown(self, tid=None): """Close the environment.""" RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) def get_repository(self, authname=None): """Return the version control repository configured for this environment. @param authname: user name for authorization """ return RepositoryManager(self).get_repository(authname) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values.""" def _create_file(fname, data=None): fd = open(fname, 'w') if data: fd.write(data) fd.close() # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.get_log_dir()) os.mkdir(self.get_htdocs_dir()) os.mkdir(os.path.join(self.path, 'plugins')) os.mkdir(os.path.join(self.path, 'wiki-macros')) # Create a few files _create_file(os.path.join(self.path, 'VERSION'), 'Trac Environment Version 1\n') _create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(os.path.join(self.path, 'conf')) _create_file(os.path.join(self.path, 'conf', 'trac.ini')) self.setup_config(load_defaults=True) for section, name, value in options: self.config.set(section, name, value) self.config.save() # Create the database DatabaseManager(self).init_db() def get_version(self, db=None): """Return the current version of the database.""" if not db: db = self.get_db_cnx() cursor = db.cursor() cursor.execute( "SELECT value FROM system WHERE name='database_version'") row = cursor.fetchone() return row and int(row[0]) def setup_config(self, load_defaults=False): """Load the configuration file.""" self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) if load_defaults: for section, default_options in self.config.defaults().iteritems(): for name, value in default_options.iteritems(): if self.config.has_site_option(section, name): value = None self.config.set(section, name, value) def get_templates_dir(self): """Return absolute path to the templates directory.""" return os.path.join(self.path, 'templates') def get_htdocs_dir(self): """Return absolute path to the htdocs directory.""" return os.path.join(self.path, 'htdocs') def get_log_dir(self): """Return absolute path to the log directory.""" return os.path.join(self.path, 'log') def setup_log(self): """Initialize the logging sub-system.""" from trac.log import logger_factory logtype = self.log_type logfile = self.log_file if logtype == 'file' and not os.path.isabs(logfile): logfile = os.path.join(self.get_log_dir(), logfile) format = self.log_format if format: format = format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', os.path.basename(self.path)) \ .replace('%(project)s', self.project_name) self.log = logger_factory(logtype, logfile, self.log_level, self.path, format=format) def get_known_users(self, cnx=None): """Generator that yields information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. This function generates one tuple for every user, of the form (username, name, email) ordered alpha-numerically by username. @param cnx: the database connection; if ommitted, a new connection is retrieved """ if not cnx: cnx = self.get_db_cnx() cursor = cnx.cursor() cursor.execute("SELECT DISTINCT s.sid, n.value, e.value " "FROM session AS s " " LEFT JOIN session_attribute AS n ON (n.sid=s.sid " " and n.authenticated=1 AND n.name = 'name') " " LEFT JOIN session_attribute AS e ON (e.sid=s.sid " " AND e.authenticated=1 AND e.name = 'email') " "WHERE s.authenticated=1 ORDER BY s.sid") for username, name, email in cursor: yield username, name, email def backup(self, dest=None): """Simple SQLite-specific backup of the database. @param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ import shutil db_str = self.config.get('trac', 'database') if not db_str.startswith('sqlite:'): raise EnvironmentError('Can only backup sqlite databases') db_name = os.path.join(self.path, db_str[7:]) if not dest: dest = '%s.%i.bak' % (db_name, self.get_version()) shutil.copy(db_name, dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" db = self.get_db_cnx() for participant in self.setup_participants: if participant.environment_needs_upgrade(db): self.log.warning('Component %s requires environment upgrade', participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. Each db version should have its own upgrade module, names upgrades/dbN.py, where 'N' is the version number (int). @param backup: whether or not to backup before upgrading @param backup_dest: name of the backup file @return: whether the upgrade was performed """ db = self.get_db_cnx() upgraders = [] for participant in self.setup_participants: if participant.environment_needs_upgrade(db): upgraders.append(participant) if not upgraders: return False if backup: self.backup(backup_dest) for participant in upgraders: participant.upgrade_environment(db) db.commit() # Database schema may have changed, so close all connections self.shutdown() return True
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. """) 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. """) message_id_hash = Option( 'notification', 'message_id_hash', 'md5', """Hash algorithm to create unique Message-ID header. ''(since 1.0.13)''""") 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) @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 get_default_format(self, transport): return self.config.get('notification', 'default_format.' + transport) or 'text/plain' def get_preferred_format(self, sid, authenticated, transport): from trac.notification.prefs import get_preferred_format return get_preferred_format(self.env, sid, authenticated, transport) or \ self.get_default_format(transport) 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, {}) key = (sid, authenticated, address) if key in package: continue package[key] = format or self.get_preferred_format( sid, authenticated, transport) for distributor in self.distributors: for transport in distributor.transports(): if transport in packages: recipients = [ (k[0], k[1], k[2], format) for k, format in packages[transport].iteritems() ] 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: if event.category == 'batchmodify': for ticket_event in event.get_ticket_change_events(self.env): subscriptions.extend( x for x in subscriber.matches(ticket_event) if x) else: 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)
class SubscriptionManagementPanel(AnnouncerTemplateProvider): implements(IPreferencePanelProvider, ITemplateStreamFilter) subscribers = ExtensionPoint(IAnnouncementSubscriber) default_subscribers = ExtensionPoint(IAnnouncementDefaultSubscriber) distributors = ExtensionPoint(IAnnouncementDistributor) formatters = ExtensionPoint(IAnnouncementFormatter) def __init__(self): self.post_handlers = { 'add-rule': self._add_rule, 'delete-rule': self._delete_rule, 'move-rule': self._move_rule, 'set-format': self._set_format } # IPreferencePanelProvider methods def get_preference_panels(self, req): yield 'subscriptions', _("Subscriptions") def render_preference_panel(self, req, panel, path_info=None): if req.method == 'POST': method_arg = req.args.get('method', '') m = re.match('^([^_]+)_(.+)', method_arg) if m: method, arg = m.groups() method_func = self.post_handlers.get(method) if method_func: method_func(arg, req) else: pass else: pass # Refresh page after saving changes. req.redirect(req.href.prefs('subscriptions')) data = { 'rules': {}, 'subscribers': [], 'formatters': ('text/plain', 'text/html'), 'selected_format': {}, 'adverbs': ('always', 'never') } desc_map = {} for i in self.subscribers: if not i.description(): continue if not req.session.authenticated and i.requires_authentication(): continue data['subscribers'].append({ 'class': i.__class__.__name__, 'description': i.description() }) desc_map[i.__class__.__name__] = i.description() for i in self.distributors: for j in i.transports(): data['rules'][j] = [] for r in Subscription.find_by_sid_and_distributor( self.env, req.session.sid, req.session.authenticated, j): if desc_map.get(r['class']): data['rules'][j].append({ 'id': r['id'], 'adverb': r['adverb'], 'description': desc_map[r['class']], 'priority': r['priority'] }) data['selected_format'][j] = r['format'] data['default_rules'] = {} defaults = [] for i in self.default_subscribers: defaults.extend(i.default_subscriptions()) for r in sorted(defaults, key=operator.itemgetter(2)): klass, dist, _, adverb = r if not data['default_rules'].get(dist): data['default_rules'][dist] = [] if desc_map.get(klass): data['default_rules'][dist].append({ 'adverb': adverb, 'description': desc_map.get(klass) }) add_stylesheet(req, 'announcer/css/announcer_prefs.css') if hasattr(Chrome(self.env), 'jenv'): return 'prefs_announcer_manage_subscriptions.html', dict( data=data), None else: return 'prefs_announcer_manage_subscriptions.html', dict(data=data) # ITemplateStreamFilter method def filter_stream(self, req, method, filename, stream, data): if re.match(r'/prefs/subscription', req.path_info): xpath_match = '//form[@id="userprefs"]//div[@class="buttons"]' stream |= Transformer(xpath_match).empty() return stream def _add_rule(self, arg, req): rule = Subscription(self.env) rule['sid'] = req.session.sid rule['authenticated'] = req.session.authenticated and 1 or 0 rule['distributor'] = arg rule['format'] = req.args.get('format-%s' % arg, '') rule['adverb'] = req.args['new-adverb-%s' % arg] rule['class'] = req.args['new-rule-%s' % arg] Subscription.add(self.env, rule) def _delete_rule(self, arg, req): Subscription.delete(self.env, arg) def _move_rule(self, arg, req): (rule_id, new_priority) = arg.split('-') if int(new_priority) >= 1: Subscription.move(self.env, rule_id, int(new_priority)) def _set_format(self, arg, req): Subscription.update_format_by_distributor_and_sid( self.env, arg, req.session.sid, req.session.authenticated, req.args['format-%s' % arg])
class AdvancedSearchPlugin(Component): implements( INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider, ITicketChangeListener, IWikiChangeListener, IWikiSyntaxProvider, ) providers = ExtensionPoint(IAdvSearchBackend) DEFAULT_PER_PAGE = 10 def _get_source_filters(self): return set(itertools.chain(*(p.get_sources() for p in self.providers))) # INavigationContributor methods def get_active_navigation_item(self, req): return 'advsearch' def get_navigation_items(self, req): if 'SEARCH_VIEW' in req.perm: label = self.config.get( 'advanced_search_plugin', 'menu_label', 'Advanced Search' ) yield ('mainnav', 'advsearch', html.A(_(label), href=self.env.href.advsearch()) ) # IPermissionRequestor methods def get_permission_actions(self): return ['SEARCH_VIEW'] # IRequestHandler methods def match_request(self, req): # TODO: add /search if search module is disabled return re.match(r'/advsearch?', req.path_info) is not None def process_request(self, req): """ Implements IRequestHandler.process_request Build a dict of search criteria from the user and request results from the active AdvancedSearchBackend. """ req.perm.assert_permission('SEARCH_VIEW') try: per_page = int(req.args.getfirst('per_page', self.DEFAULT_PER_PAGE)) except ValueError: self.log.warn('Could not set per_page to %s' % req.args.getfirst('per_page')) per_page = self.DEFAULT_PER_PAGE try: page = int(req.args.getfirst('page', 1)) except ValueError: page = 1 data = { 'source': self._get_filter_dicts(req.args), 'author': [auth for auth in req.args.getlist('author') if auth], 'date_start': req.args.getfirst('date_start'), 'date_end': req.args.getfirst('date_end'), 'q': req.args.get('q'), 'start_points': StartPoints.parse_args(req.args, self.providers), 'per_page': per_page, 'ticket_statuses': self._get_ticket_statuses(req.args), 'source_filter_defaults': self.config.get('advanced_search_plugin', 'source_filter_defaults', '').split(','), } # Initial page request if not any((data['q'], data['author'], data['date_start'], data['date_end'])): return self._send_response(req, data) # Look for quickjump quickjump = self._get_quickjump(req, data['q']) if quickjump: req.redirect(quickjump) # perform query using backend if q is set result_map = {} total_count = 0 for provider in self.providers: result_count, result_list = 0, [] try: result_count, result_list = provider.query_backend(data) except SearchBackendException, e: add_warning(req, _('SearchBackendException: %s' % e)) total_count += result_count result_map[provider.get_name()] = result_list if not total_count: return self._send_response(req, data) data['page'] = page results = self._merge_results(result_map, per_page) self._add_href_to_results(results) data['results'] = Paginator( results, page=page-1, max_per_page=per_page, num_items=total_count ) # pagination next/prev links if data['results'].has_next_page: data['start_points'] = StartPoints.format(results, data['start_points']) return self._send_response(req, data)
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file * an SQLite database (stores tickets, wiki pages...) * project-specific templates and plugins * wiki and ticket attachments """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. (''since 0.11'')""") base_url = Option( 'trac', 'base_url', '', """Reference URL for the Trac deployment. This is the base URL that will be used when producing documents that will be used outside of the web browsing context, like for example when inserting URLs pointing to Trac resources in notification e-mails.""") base_url_for_redirect = BoolOption( 'trac', 'use_base_url_for_redirect', False, """Optionally use `[trac] base_url` for redirects. In some configurations, usually involving running Trac behind a HTTP proxy, Trac can't automatically reconstruct the URL that is used to access it. You may need to use this option to force Trac to use the `base_url` setting also for redirects. This introduces the obvious limitation that this environment will only be usable when accessible from that URL, as redirects are frequently used. ''(since 0.10.5)''""") secure_cookies = BoolOption( 'trac', 'secure_cookies', False, """Restrict cookies to HTTPS connections. When true, set the `secure` flag on all cookies so that they are only sent to the server on HTTPS connections. Use this if your Trac instance is only accessible through HTTPS. (''since 0.11.2'')""") project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option( 'project', 'url', '', """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. (''since 0.11.3'')""") project_footer = Option( 'project', 'footer', N_('Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>'), """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = Option( 'logging', 'log_type', 'none', """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""") log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = Option( 'logging', 'log_level', 'DEBUG', """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""") log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: Trac[$(module)s] $(levelname)s: $(message)s In addition to regular key names supported by the Python logger library (see http://docs.python.org/library/logging.html), one could use: - $(path)s the path for the current environment - $(basename)s the last path component of the current environment - $(project)s the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the ConfigParser itself. Example: `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` ''(since 0.10.5)''""") def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. @param path: the absolute path to the Trac environment @param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. @param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = path self.systeminfo = [] self._href = self._abs_href = None if create: self.create(options) else: self.verify() self.setup_config() if create: for setup_participant in self.setup_participants: setup_participant.environment_created() def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = self.systeminfo[:] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) info.sort(key=lambda (name, version): (name != 'Trac', name.lower())) return info # ISystemInfoProvider methods def get_system_info(self): from trac import core, __version__ as VERSION yield 'Trac', get_pkginfo(core).get('version', VERSION) yield 'Python', sys.version yield 'setuptools', setuptools.__version__ from trac.util.datefmt import pytz if pytz is not None: yield 'pytz', pytz.__version__ def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @property def _component_rules(self): try: return self._rules except AttributeError: self._rules = {} for name, value in self.config.options('components'): if name.endswith('.*'): name = name[:-2] self._rules[name.lower()] = value.lower() in ('enabled', 'on') return self._rules def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns `False`, the component does not get activated. If it returns `None`, the component only gets activated if it is located in the `plugins` directory of the enironment. """ component_name = self._component_name(cls) # Disable the pre-0.11 WebAdmin plugin # Please note that there's no recommendation to uninstall the # plugin because doing so would obviously break the backwards # compatibility that the new integration administration # interface tries to provide for old WebAdmin extensions if component_name.startswith('webadmin.'): self.log.info('The legacy TracWebAdmin plugin has been ' 'automatically disabled, and the integrated ' 'administration interface will be used ' 'instead.') return False rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package are enabled return component_name.startswith('trac.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" fd = open(os.path.join(self.path, 'VERSION'), 'r') try: assert fd.read(26) == 'Trac Environment Version 1' finally: fd.close() def get_db_cnx(self): """Return a database connection from the connection pool (deprecated) Use `with_transaction` for obtaining a writable database connection and `get_read_db` for anything else. """ return get_read_db(self) def with_transaction(self, db=None): """Decorator for transaction functions. See `trac.db.api.with_transaction` for detailed documentation.""" return with_transaction(self, db) def get_read_db(self): """Return a database connection for read purposes. See `trac.db.api.get_read_db` for detailed documentation.""" return get_read_db(self) def shutdown(self, tid=None): """Close the environment.""" RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: self.log.removeHandler(self._log_handler) self._log_handler.flush() self._log_handler.close() del self._log_handler def get_repository(self, reponame=None, authname=None): """Return the version control repository with the given name, or the default repository if `None`. The standard way of retrieving repositories is to use the methods of `RepositoryManager`. This method is retained here for backward compatibility. @param reponame: the name of the repository @param authname: the user name for authorization (not used anymore, left here for compatibility with 0.11) """ return RepositoryManager(self).get_repository(reponame) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values. If options contains ('inherit', 'file'), default values will not be loaded; they are expected to be provided by that file or other options. """ # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.get_log_dir()) os.mkdir(self.get_htdocs_dir()) os.mkdir(os.path.join(self.path, 'plugins')) # Create a few files create_file(os.path.join(self.path, 'VERSION'), 'Trac Environment Version 1\n') create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(os.path.join(self.path, 'conf')) create_file(os.path.join(self.path, 'conf', 'trac.ini.sample')) config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the database DatabaseManager(self).init_db() def get_version(self, db=None, initial=False): """Return the current version of the database. If the optional argument `initial` is set to `True`, the version of the database used at the time of creation will be returned. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since 0.11: """ if not db: db = self.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT value FROM system " "WHERE name='%sdatabase_version'" % (initial and 'initial_' or '')) row = cursor.fetchone() return row and int(row[0]) def setup_config(self): """Load the configuration file.""" self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini')) self.setup_log() from trac.loader import load_components plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) def get_templates_dir(self): """Return absolute path to the templates directory.""" return os.path.join(self.path, 'templates') def get_htdocs_dir(self): """Return absolute path to the htdocs directory.""" return os.path.join(self.path, 'htdocs') def get_log_dir(self): """Return absolute path to the log directory.""" return os.path.join(self.path, 'log') def setup_log(self): """Initialize the logging sub-system.""" from trac.log import logger_handler_factory logtype = self.log_type logfile = self.log_file if logtype == 'file' and not os.path.isabs(logfile): logfile = os.path.join(self.get_log_dir(), logfile) format = self.log_format if format: format = format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', os.path.basename(self.path)) \ .replace('%(project)s', self.project_name) self.log, self._log_handler = logger_handler_factory(logtype, logfile, self.log_level, self.path, format=format) from trac import core, __version__ as VERSION self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, get_pkginfo(core).get('version', VERSION)) def get_known_users(self, cnx=None): """Generator that yields information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. This function generates one tuple for every user, of the form (username, name, email) ordered alpha-numerically by username. @param cnx: the database connection; if ommitted, a new connection is retrieved """ if not cnx: cnx = self.get_db_cnx() cursor = cnx.cursor() cursor.execute("SELECT DISTINCT s.sid, n.value, e.value " "FROM session AS s " " LEFT JOIN session_attribute AS n ON (n.sid=s.sid " " and n.authenticated=1 AND n.name = 'name') " " LEFT JOIN session_attribute AS e ON (e.sid=s.sid " " AND e.authenticated=1 AND e.name = 'email') " "WHERE s.authenticated=1 ORDER BY s.sid") for username, name, email in cursor: yield username, name, email def backup(self, dest=None): """Create a backup of the database. @param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" db = self.get_db_cnx() for participant in self.setup_participants: if participant.environment_needs_upgrade(db): self.log.warning('Component %s requires environment upgrade', participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. @param backup: whether or not to backup before upgrading @param backup_dest: name of the backup file @return: whether the upgrade was performed """ upgraders = [] db = self.get_read_db() for participant in self.setup_participants: if participant.environment_needs_upgrade(db): upgraders.append(participant) if not upgraders: return if backup: self.backup(backup_dest) for participant in upgraders: self.log.info("%s.%s upgrading...", participant.__module__, participant.__class__.__name__) with_transaction(self)(participant.upgrade_environment) # Database schema may have changed, so close all connections DatabaseManager(self).shutdown() return True def _get_href(self): if not self._href: self._href = Href(urlsplit(self.abs_href.base)[2]) return self._href href = property(_get_href, 'The application root path') def _get_abs_href(self): if not self._abs_href: if not self.base_url: self.log.warn('base_url option not set in configuration, ' 'generated links may be incorrect') self._abs_href = Href('') else: self._abs_href = Href(self.base_url) return self._abs_href abs_href = property(_get_abs_href, 'The application URL')
class Daemon(Component): implements(IRequestHandler, IAdminPanelProvider, ITemplateProvider) jobs = ExtensionPoint(ICronJob) engine = None entries = {} def __init__(self): Component.__init__(self) self.entries = self.get_entries() self.__class__.engine = self t = threading.Thread(target=self.poll) t.daemon = True t.start() # NOTE: # I cannot use compmgr.is_enabled() because in case that the egg in project/plugin folder # always enable even if no configuration line in [component] section. # -> mandatory adding the configuration line in trac.ini # NOTE: # I cannot use compmgr.is_enabled() because in case that execute with tracd-script # and disabled in configuration always the method returns true. # I have to see the configuration not via compmgr. def poll(self): key = '%s.%s' % (self.__module__, self.__class__.__name__) while True: if not self.config.getbool('components', key): self.env.log.debug("cron engine has disabled ... exiting.") break # exit when disabled if self != self.__class__.engine: self.env.log.debug( "cron engine rebooted ... old thread is exiting.") break # exit when another thread is created # runs every interval in seconds now = datetime.now() for job in self.jobs: # decide execute the job or not entry = self.entries[getkey(job)] if entry and \ now.minute in entry[0] and \ now.hour in entry[1] and \ now.month in entry[3] and \ (now.day in entry[2] or \ (now.weekday() + 1) % 7 in entry[4]): try: job.run() except: self.env.log.exception("Unexpected error:") else: # self.env.log.debug('Skipped: ' + job.__repr__()) pass time.sleep(10 - datetime.now().second % 10) pass # thread end self.env.log.debug("exit from polling loop. " + self.__repr__()) def parse_entry(self, arg): # str is not contains space char # throws ValueError if int(str) fails if arg == '*': # "*" return ANY result = [] params = arg.split( ',') # "1,2,3-5,6-10/2" -> ["1", "2", "3-5", "6-10/2"] for param in params: if not "-" in param: result.append(int(param)) # "1", "2" -> [ ..., 1, 2] continue param = param.split( '-', 1) # "3-5" -> ["3", "5"] # "6-10/2" -> ["6", "10/2"] if not '/' in param[1]: result.extend(range(int(param[0]), int(param[1]) + 1)) # [ ..., 3, 4, 5] continue param[1], step = param[1].split('/', 1) result.extend(range(int(param[0]), int(param[1]) + 1, int(step))) # [ ..., 6, 8, 10] return result def load_entry(self, entry): # http://svn.freebsd.org/base/head/usr.sbin/cron/lib/entry.c entry = entry.strip() if entry.startswith('#'): return [NEVER, NEVER, NEVER, NEVER, NEVER] elif entry.startswith('@reboot'): return False # Not Implemented elif entry.startswith('@yearly'): return [ZERO, ZERO, ONE, ONE, ANY] elif entry.startswith('@annually'): return [ZERO, ZERO, ONE, ONE, ANY] elif entry.startswith('@monthly'): return [ZERO, ZERO, ONE, ANY, ANY] elif entry.startswith('@weekly'): return [ZERO, ZERO, ANY, ANY, ZERO] elif entry.startswith('@daily'): return [ZERO, ZERO, ANY, ANY, ANY] elif entry.startswith('@midnight'): return [ZERO, ZERO, ANY, ANY, ANY] elif entry.startswith('@hourly'): return [ZERO, ANY, ANY, ANY, ANY] elif entry.startswith('@every_minute'): return [ANY, ANY, ANY, ANY, ANY] elif entry.startswith('@every_second'): return False # Not Implemented else: # about to parse numerics entries = entry.split(' ') if len(entries) != 5: return False # Not Implemented try: return map(self.parse_entry, entries) except: # ValueError return False def get_entries(self): result = {} for job in self.jobs: key = getkey(job) time_field = self.config.get(section, key, False) or \ (job.default_time() if 'default_time' in dir(job) else '# 0 * * * *') result[key] = self.load_entry(time_field) return result # IRequestHandler Methods def match_request(self, req): return False def process_request(self, req): pass # IAdminPanelProvider methods def get_admin_panels(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('cron', 'Cron', 'crontab', 'Crontab') def render_admin_panel(self, req, category, page, path_info): req.perm.require('TICKET_ADMIN') if req.method == 'POST': darty = False for key in req.args.keys(): if key.startswith('cron_'): entry_string = req.args.get(key) entry = self.load_entry(entry_string) if entry: self.entries[key[5:]] = entry self.config.set(section, key[5:], entry_string) darty = True else: # notify.append({key[5:], entry_string}) add_warning( req, "cycle \"%s\" cannot recognized for %s. " % (entry_string, key[5:])) pass if darty and len(req.chrome['warnings']) == 0: self.config.save() req.redirect(req.href.admin(category, page)) data = [] for job in self.jobs: key = getkey(job) entry_string = req.args.get('cron_' + key) or \ self.config.get(section, key, False) or \ (job.default_time() if 'default_time' in dir(job) else '# 0 * * * *') data.append({'name': key, 'cycle': entry_string}) return 'crontab.html', {'jobs': data} # ITemplateProvider methods def get_templates_dirs(self): return [ResourceManager().resource_filename(__name__, 'templates')] def get_htdocs_dirs(self): return []
class NotificationPreferences(Component): implements(IPreferencePanelProvider, ITemplateProvider) subscribers = ExtensionPoint(INotificationSubscriber) distributors = ExtensionPoint(INotificationDistributor) formatters = ExtensionPoint(INotificationFormatter) def __init__(self): self.post_handlers = { 'add-rule': self._add_rule, 'delete-rule': self._delete_rule, 'move-rule': self._move_rule, 'replace': self._replace_rules, } # IPreferencePanelProvider methods def get_preference_panels(self, req): yield ('notification', _('Notifications')) def render_preference_panel(self, req, panel, path_info=None): if req.method == 'POST': action_arg = req.args.getfirst('action', '').split('_', 1) if len(action_arg) == 2: action, arg = action_arg handler = self.post_handlers.get(action) if handler: handler(arg, req) add_notice(req, _("Your preferences have been saved.")) req.redirect(req.href.prefs('notification')) rules = {} subscribers = [] formatters = {} selected_format = {} defaults = [] for i in self.subscribers: description = i.description() if not description: continue if not req.session.authenticated and i.requires_authentication(): continue subscribers.append({ 'class': i.__class__.__name__, 'description': description }) if hasattr(i, 'default_subscriptions'): defaults.extend(i.default_subscriptions()) desc_map = dict((s['class'], s['description']) for s in subscribers) for t in self._iter_transports(): rules[t] = [] formatters[t] = self._get_supported_styles(t) selected_format[t] = req.session.get('notification.format.%s' % t) for r in self._iter_rules(req, t): description = desc_map.get(r['class']) if description: values = {'description': description} values.update( (key, r[key]) for key in ('id', 'adverb', 'class', 'priority')) rules[t].append(values) default_rules = {} for r in sorted(defaults, key=itemgetter(3)): # sort by priority klass, dist, format, priority, adverb = r default_rules.setdefault(dist, []) description = desc_map.get(klass) if description: default_rules[dist].append({ 'adverb': adverb, 'description': description }) data = { 'rules': rules, 'subscribers': subscribers, 'formatters': formatters, 'selected_format': selected_format, 'default_rules': default_rules, 'adverbs': ('always', 'never'), 'adverb_labels': { 'always': _("Notify"), 'never': _("Never notify") } } Chrome(self.env).add_jquery_ui(req) return 'prefs_notification.html', dict(data=data) # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): resource_dir = resource_filename('trac.notification', 'templates') return [resource_dir] # Internal methods def _add_rule(self, arg, req): rule = Subscription(self.env) rule['sid'] = req.session.sid rule['authenticated'] = 1 if req.session.authenticated else 0 rule['distributor'] = arg rule['format'] = req.args.get('format-%s' % arg, '') rule['adverb'] = req.args['new-adverb-%s' % arg] rule['class'] = req.args['new-rule-%s' % arg] Subscription.add(self.env, rule) def _delete_rule(self, arg, req): session = req.session Subscription.delete(self.env, arg, session.sid, session.authenticated) def _move_rule(self, arg, req): tokens = [as_int(val, 0) for val in arg.split('-', 1)] if len(tokens) == 2: rule_id, priority = tokens if rule_id > 0 and priority > 0: session = req.session Subscription.move(self.env, rule_id, priority, session.sid, session.authenticated) def _replace_rules(self, arg, req): subscriptions = [] for transport in self._iter_transports(): format_ = req.args.getfirst('format-' + transport) format_ = self._normalize_format(format_, transport) req.session.set('notification.format.%s' % transport, format_, '') adverbs = req.args.getlist('adverb-' + transport) classes = req.args.getlist('class-' + transport) for idx in xrange(min(len(adverbs), len(classes))): subscriptions.append({ 'distributor': transport, 'format': format_, 'adverb': adverbs[idx], 'class': classes[idx] }) sid = req.session.sid authenticated = req.session.authenticated with self.env.db_transaction: Subscription.replace_all(self.env, sid, authenticated, subscriptions) def _iter_rules(self, req, transport): session = req.session for r in Subscription.find_by_sid_and_distributor( self.env, session.sid, session.authenticated, transport): yield r def _iter_transports(self): for distributor in self.distributors: for transport in distributor.transports(): yield transport def _get_supported_styles(self, transport): styles = set() for formatter in self.formatters: for style, realm_ in formatter.get_supported_styles(transport): styles.add(style) return sorted(styles) def _normalize_format(self, format_, transport): if format_: styles = self._get_supported_styles(transport) if format_ in styles: return format_ return ''
class RelationsSystem(Component): PARENT_RELATION_TYPE = 'parent' CHILDREN_RELATION_TYPE = 'children' changing_listeners = ExtensionPoint(IRelationChangingListener) all_validators = ExtensionPoint(IRelationValidator) global_validators = OrderedExtensionsOption( 'bhrelations', 'global_validators', IRelationValidator, 'NoSelfReferenceValidator, ExclusiveValidator, BlockerValidator', include_missing=False, doc="""Validators used to validate all relations, regardless of their type.""", doc_domain='bhrelations') duplicate_relation_type = Option( 'bhrelations', 'duplicate_relation', 'duplicateof', "Relation type to be used with the resolve as duplicate workflow.", doc_domain='bhrelations') def __init__(self): import pkg_resources locale_dir = pkg_resources.resource_filename(__name__, 'locale') add_domain(self.env.path, locale_dir) links, labels, validators, blockers, copy_fields, exclusive = \ self._parse_config() self._links = links self._labels = labels self._validators = validators self._blockers = blockers self._copy_fields = copy_fields self._exclusive = exclusive self.link_ends_map = {} for end1, end2 in self.get_ends(): self.link_ends_map[end1] = end2 if end2 is not None: self.link_ends_map[end2] = end1 def get_ends(self): return self._links def add(self, source_resource_instance, destination_resource_instance, relation_type, comment=None, author=None, when=None): source = ResourceIdSerializer.get_resource_id_from_instance( self.env, source_resource_instance) destination = ResourceIdSerializer.get_resource_id_from_instance( self.env, destination_resource_instance) if relation_type not in self.link_ends_map: raise UnknownRelationType(relation_type) if when is None: when = datetime.now(utc) relation = Relation(self.env) relation.source = source relation.destination = destination relation.type = relation_type relation.comment = comment relation.author = author relation.when = when self.add_relation(relation) return relation def get_reverted_relation(self, relation): """Return None if relation is one way""" other_end = self.link_ends_map[relation.type] if other_end: return relation.clone_reverted(other_end) def add_relation(self, relation): self.validate(relation) with self.env.db_transaction: relation.insert() reverted_relation = self.get_reverted_relation(relation) if reverted_relation: reverted_relation.insert() for listener in self.changing_listeners: listener.adding_relation(relation) def delete(self, relation_id, when=None): if when is None: when = datetime.now(utc) relation = Relation.load_by_relation_id(self.env, relation_id) source = relation.source destination = relation.destination relation_type = relation.type with self.env.db_transaction: cloned_relation = relation.clone() relation.delete() other_end = self.link_ends_map[relation_type] if other_end: reverted_relation = Relation(self.env, keys=dict( source=destination, destination=source, type=other_end, )) reverted_relation.delete() for listener in self.changing_listeners: listener.deleting_relation(cloned_relation, when) from bhrelations.notification import RelationNotifyEmail RelationNotifyEmail(self.env).notify(cloned_relation, deleted=when) def delete_resource_relations(self, resource_instance): sql = "DELETE FROM " + Relation.get_table_name() + \ " WHERE source=%s OR destination=%s" full_resource_id = ResourceIdSerializer.get_resource_id_from_instance( self.env, resource_instance) with self.env.db_transaction as db: db(sql, (full_resource_id, full_resource_id)) def _debug_select(self): """The method is used for debug purposes""" sql = "SELECT id, source, destination, type FROM bloodhound_relations" with self.env.db_query as db: return [db(sql)] def get_relations(self, resource_instance): relation_list = [] for relation in self._select_relations_for_resource_instance( resource_instance): relation_list.append( dict( relation_id=relation.get_relation_id(), destination_id=relation.destination, destination=ResourceIdSerializer.get_resource_by_id( relation.destination), type=relation.type, comment=relation.comment, when=relation.when, author=relation.author, )) return relation_list def _select_relations_for_resource_instance(self, resource): resource_full_id = ResourceIdSerializer.get_resource_id_from_instance( self.env, resource) return self._select_relations(resource_full_id) def _select_relations(self, source=None, resource_type=None, destination=None): #todo: add optional paging for possible umbrella tickets with #a lot of child tickets where = dict() if source: where["source"] = source if resource_type: where["type"] = resource_type order_by = ["destination"] else: order_by = ["type", "destination"] if destination: where["destination"] = destination return Relation.select(self.env, where=where, order_by=order_by) def _parse_config(self): links = [] labels = {} validators = {} blockers = {} copy_fields = {} exclusive = set() config = self.config[RELATIONS_CONFIG_NAME] for name in [ option for option, _ in config.options() if '.' not in option ]: reltypes = config.getlist(name) if not reltypes: continue if len(reltypes) == 1: reltypes += [None] links.append(tuple(reltypes)) custom_validators = self._parse_validators(config, name) for rel in filter(None, reltypes): labels[rel] = \ config.get(rel + '.label') or rel.capitalize() blockers[rel] = \ config.getbool(rel + '.blocks', default=False) if config.getbool(rel + '.exclusive'): exclusive.add(rel) validators[rel] = custom_validators # <end>.copy_fields may be absent or intentionally set empty. # config.getlist() will return [] in either case, so check that # the key is present before assigning the value cf_key = '%s.copy_fields' % rel if cf_key in config: copy_fields[rel] = config.getlist(cf_key) return links, labels, validators, blockers, copy_fields, exclusive def _parse_validators(self, section, name): custom_validators = set('%sValidator' % validator for validator in set( section.getlist(name + '.validators', [], ',', True))) validators = [] if custom_validators: for impl in self.all_validators: if impl.__class__.__name__ in custom_validators: validators.append(impl) return validators def validate(self, relation): """ Validate the relation using the configured validators. Validation is always run on the relation with master type. """ backrel = self.get_reverted_relation(relation) if backrel and (backrel.type, relation.type) in self._links: relation = backrel for validator in self.global_validators: validator.validate(relation) for validator in self._validators.get(relation.type, ()): validator.validate(relation) def is_blocker(self, relation_type): return self._blockers[relation_type] def render_relation_type(self, end): return self._labels[end] def get_relation_types(self): return self._labels def find_blockers(self, resource_instance, is_blocker_method): # tbd: do we blocker finding to be recursive all_blockers = [] for relation in self._select_relations_for_resource_instance( resource_instance): if self.is_blocker(relation.type): resource = ResourceIdSerializer.get_resource_by_id( relation.destination) resource_instance = is_blocker_method(resource) if resource_instance is not None: all_blockers.append(resource_instance) # blockers = self._recursive_find_blockers( # relation, is_blocker_method) # if blockers: # all_blockers.extend(blockers) return all_blockers def get_resource_name(self, resource_id): resource = ResourceIdSerializer.get_resource_by_id(resource_id) return get_resource_shortname(self.env, resource)
def __init__(self, section, name, interface, default=None, doc='', doc_domain='tracini'): Option.__init__(self, section, name, default, doc, doc_domain) self.xtnpt = ExtensionPoint(interface)
class OidcPlugin(Component): """ Authenticate via OpenID Connect """ implements(INavigationContributor, IRequestHandler) RETURN_URL_SKEY = 'trac_oidc.return_url' client_secret_file = PathOption( 'trac_oidc', 'client_secret_file', 'client_secret.json', """Path to client_secret file. Relative paths are interpreted relative to the ``conf`` subdirectory of the trac environment.""") # deprecated absolute_trust_root = BoolOption( 'openid', 'absolute_trust_root', 'true', """Whether we should use absolute trust root or by project.""") login_managers = ExtensionPoint(ILoginManager) def __init__(self): # We should show our own "Logout" link only if the stock # LoginModule is disabled. self.show_logout_link = not is_component_enabled(self.env, LoginModule) self.userdb = UserDatabase(self.env) # INavigationContributor methods def get_active_navigation_item(self, req): return 'trac_oidc.login' def get_navigation_items(self, req): oidc_href = req.href.trac_oidc path_qs = req.path_info if req.query_string: path_qs += '?' + req.query_string if not req.authname or req.authname == 'anonymous': # Not logged in, show login link login_link = tag.a(_('Login using Google'), href=oidc_href('login', return_to=path_qs)) yield 'metanav', 'trac_oidc.login', login_link elif self.show_logout_link: # Logged in and LoginModule is disabled, show logout link yield ('metanav', 'trac_oidc.login', _('logged in as %(user)s', user=req.authname)) yield ('metanav', 'trac_oidc.logout', logout_link(oidc_href, return_to=path_qs)) # IRequestHandler methods def match_request(self, req): path_info = req.path_info if path_info == '/login' and self.show_logout_link: # Stock LoginModule is disabled, so handle default /login too return True return path_info in ('/trac_oidc/login', '/trac_oidc/logout', '/trac_oidc/redirect') def process_request(self, req): if req.path_info.endswith('/logout'): return_url = self._get_return_url(req) self._forget_user(req) return req.redirect(return_url) elif req.path_info.endswith('/login'): # Start the login process by redirectory to OP req.session[self.RETURN_URL_SKEY] = self._get_return_url(req) authenticator = self._get_authenticator(req) return req.redirect(authenticator.get_auth_url(req)) elif req.path_info.endswith('/redirect'): # Finish the login process after redirect from OP return_url = req.session.pop(self.RETURN_URL_SKEY, req.abs_href()) id_token = self._retrieve_id(req) if id_token: authname = self._find_or_create_session(req, id_token) assert authname self.log.debug("Logging in as %r", authname) self._remember_user(req, authname) return req.redirect(return_url) # private methods def _retrieve_id(self, req): """ Retrieve oidc id_token from provider. Returns ``None`` if authentication was unsuccessful for any reason. """ authenticator = self._get_authenticator(req) try: return authenticator.get_identity(req) except AuthenticationFailed as ex: self.log.info("Authentication failed: %s", ex) add_warning(req, "Authentication failed: %s", ex) except AuthenticationError as ex: self.log.error("Authentication error: %s", ex) add_warning(req, "Authentication error: %s", ex) def _find_or_create_session(self, req, id_token): """ Find or create authenticated session for subject. """ userdb = self.userdb authname = userdb.find_session(id_token) if not authname: # There is no authenticated session for the user, # create a new one # XXX: should it be configurable whether this happens? authname = userdb.create_session(id_token) add_notice( req, _( "Hello! You appear to be new here. " "A new authenticated session with " "username '%(authname)s' has been created for you.", authname=authname)) return authname def _remember_user(self, req, authname): for lm in self.login_managers: lm.remember_user(req, authname) def _forget_user(self, req): for lm in self.login_managers: lm.forget_user(req) def _get_authenticator(self, req): conf_dir = os.path.join(self.env.path, 'conf') client_secret_file = os.path.join(conf_dir, self.client_secret_file) redirect_url = req.abs_href.trac_oidc('redirect') openid_realm = self._get_openid_realm(req) self.log.debug('openid_realm = %r', openid_realm) return Authenticator(client_secret_file, redirect_url, openid_realm, self.log) def _get_openid_realm(self, req): """ Get the OpenID realm. This computes the OpenID realm in exactly the same manner that the ``TracAuthOpenID`` plugin does. Note that I'm not sure this is really the “right” way to do it, but, since we want to get back the same identity URLs from google as we did using ``TracAuthOpenID``, here we are. """ href = req.href() abs_href = self.env.abs_href() if href and abs_href.endswith(href): base_url = abs_href[:-len(href)] else: # pragma: NO COVER base_url = abs_href if self.absolute_trust_root: path = '/' else: path = href return base_url + path @staticmethod def _get_return_url(req): return_to = req.args.getfirst('return_to', '/') # We expect return_to to be a URL relative to the trac's base_path. # Be paranoid about this. scheme, netloc, path, query, anchor = urlsplit(return_to) if scheme or netloc or '..' in path.split('/') or anchor: # return url looks suspicious, ignore it. return req.abs_href() return_url = req.abs_href(path) if query: return_url += '?' + query return return_url
def __init__(self, section, name, interface, default=None, doc=''): Option.__init__(self, section, name, default, doc) self.xtnpt = ExtensionPoint(interface)
def __init__(self, section, name, interface, default=None, include_missing=True, doc='', doc_domain='tracini'): ListOption.__init__(self, section, name, default, doc=doc, doc_domain=doc_domain) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing
def __init__(self, section, name, interface, default=None, include_missing=True, doc=''): ListOption.__init__(self, section, name, default, doc=doc) self.xtnpt = ExtensionPoint(interface) self.include_missing = include_missing
class DownloadsConsoleAdmin(Component): """ The consoleadmin module implements downloads plugin administration via trac-admin command. """ implements(IAdminCommandProvider) # Download change listeners. change_listeners = ExtensionPoint(IDownloadChangeListener) # Configuration options. consoleadmin_user = Option( 'downloads', 'consoleadmin_user', 'anonymous', doc= 'User whos permissons will be used to upload download. User should have TAGS_MODIFY permissons.' ) # IAdminCommandProvider def get_admin_commands(self): yield ('download list', '', 'Show uploaded downloads', None, self._do_list) yield ('download add', '<file> [description=<description>]' ' [author=<author>]\n [tags="<tag1> <tag2> ..."]' ' [component=<component>] [version=<version>]\n' ' [platform=<platform>]' ' [type=<type>]', 'Add new download', None, self._do_add) yield ('download remove', '<filename> | <download_id>', 'Remove uploaded download', None, self._do_remove) # Internal methods. def _do_list(self): # Get downloads API component. api = self.env[DownloadsApi] # Create context. context = Context('downloads-consoleadmin') db = self.env.get_db_cnx() context.cursor = db.cursor() # Print uploded download downloads = api.get_downloads(context) print_table( [(download['id'], download['file'], pretty_size( download['size']), format_datetime(download['time']), download['component'], download['version'], download['platform']['name'], download['type']['name']) for download in downloads], [ 'ID', 'Filename', 'Size', 'Uploaded', 'Component', 'Version', 'Platform', 'Type' ]) def _do_add(self, filename, *arguments): # Get downloads API component. api = self.env[DownloadsApi] # Create context. context = Context('downloads-consoleadmin') db = self.env.get_db_cnx() context.cursor = db.cursor() context.req = FakeRequest(self.env, self.consoleadmin_user) # Be sure, we have correct path req_path = conf.getEnvironmentDownloadsPath(self.env) # Convert relative path to absolute. if not os.path.isabs(filename): filename = os.path.join(req_path, filename) # Open file object. file, filename, file_size = self._get_file(filename) # Create download dictionary from arbitrary attributes. download = { 'file': filename, 'size': file_size, 'time': to_timestamp(datetime.now(utc)), 'count': 0 } # Read optional attributes from arguments. for argument in arguments: # Check correct format. argument = argument.split("=") if len(argument) != 2: AdminCommandError( _('Invalid format of download attribute:' ' %(value)s', value=argument)) name, value = argument # Check known arguments. if not name in ('description', 'author', 'tags', 'component', 'version', 'platform', 'type'): raise AdminCommandError( _('Invalid download attribute: %(value)s', value=name)) # Transform platform and type name to ID. if name == 'platform': value = api.get_platform_by_name(context, value)['id'] elif name == 'type': value = api.get_type_by_name(context, value)['id'] # Add attribute to download. download[name] = value self.log.debug(download) # Upload file to DB and file storage. api.store_download(context, download, file) # Close input file and commit changes in DB. file.close() db.commit() def _do_remove(self, identifier): # Get downloads API component. api = self.env[DownloadsApi] # Create context. context = Context('downloads-consoleadmin') db = self.env.get_db_cnx() context.cursor = db.cursor() context.req = FakeRequest(self.env, self.consoleadmin_user) # Get download by ID or filename. try: download_id = int(identifier) download = api.get_download(context, download_id) except ValueError: download = api.get_download_by_file(context, identifier) # Check if download exists. if not download: raise AdminCommandError( _('Invalid download identifier: %(value)s', value=identifier)) # Delete download by ID. api.remove_download(context, download) # Commit changes in DB. db.commit() def _get_file(self, filename): # Open file and get its size file = open(filename, 'rb') size = os.fstat(file.fileno())[6] # Check non-emtpy file. if size == 0: raise TracError('Can\'t upload empty file.') # Try to normalize the filename to unicode NFC if we can. # Files uploaded from OS X might be in NFD. filename = unicodedata.normalize('NFC', to_unicode(filename, 'utf-8')) filename = filename.replace('\\', '/').replace(':', '/') filename = os.path.basename(filename) return file, filename, size