def expand_macro(self, formatter, name, args): from trac.config import ConfigSection, Option section_filter = key_filter = '' args, kw = parse_args(args) if args: section_filter = args.pop(0).strip() if args: key_filter = args.pop(0).strip() def getdoc(option_or_section): doc = to_unicode(option_or_section.__doc__) if doc: doc = dgettext(option_or_section.doc_domain, doc) return doc registry = ConfigSection.get_registry(self.compmgr) sections = dict((name, getdoc(section)) for name, section in registry.iteritems() if name.startswith(section_filter)) registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in registry.iteritems(): if section.startswith(section_filter): options.setdefault(section, {})[key] = option sections.setdefault(section, '') def default_cell(option): default = option.default if default is True: default = 'true' elif default is False: default = 'false' elif default == 0: default = '0.0' if isinstance(default, float) else '0' elif default: default = ', '.join(to_unicode(val) for val in default) \ if isinstance(default, (list, tuple)) \ else to_unicode(default) else: return tag.td(_("(no default)"), class_='nodefault') return tag.td(tag.code(default), class_='default') return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), tag.table(class_='wiki')(tag.tbody( tag.tr(tag.td(tag.tt(option.name)), tag.td(format_to_oneliner( self.env, formatter.context, getdoc(option))), default_cell(option)) for option in sorted(options.get(section, {}).itervalues(), key=lambda o: o.name) if option.name.startswith(key_filter)))) for section, section_doc in sorted(sections.iteritems()))
def expand_macro(self, formatter, name, content): from trac.config import ConfigSection, Option section_filter = key_filter = '' args, kw = parse_args(content) if args: section_filter = args.pop(0).strip() if args: key_filter = args.pop(0).strip() def getdoc(option_or_section): doc = to_unicode(option_or_section.__doc__) if doc: doc = dgettext(option_or_section.doc_domain, doc) return doc registry = ConfigSection.get_registry(self.compmgr) sections = dict((name, getdoc(section)) for name, section in registry.iteritems() if name.startswith(section_filter)) registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in registry.iteritems(): if section.startswith(section_filter): options.setdefault(section, {})[key] = option sections.setdefault(section, '') def default_cell(option): default = option.default if default is not None and default != '': return tag.td(tag.code(option.dumps(default)), class_='default') else: return tag.td(_("(no default)"), class_='nodefault') return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), tag.table(class_='wiki')(tag.tbody( tag.tr(tag.td(tag.tt(option.name)), tag.td(format_to_oneliner( self.env, formatter.context, getdoc(option))), default_cell(option), class_='odd' if idx % 2 else 'even') for idx, option in enumerate(sorted(options.get(section, {}).itervalues(), key=lambda o: o.name)) if option.name.startswith(key_filter)))) for section, section_doc in sorted(sections.iteritems()))
def expand_macro(self, formatter, name, args): from trac.config import ConfigSection, Option section_filter = key_filter = '' args, kw = parse_args(args) if args: section_filter = args.pop(0).strip() if args: key_filter = args.pop(0).strip() registry = ConfigSection.get_registry(self.compmgr) sections = dict( (name, dgettext(section.doc_domain, to_unicode(section.__doc__))) for name, section in registry.iteritems() if name.startswith(section_filter)) registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in registry.iteritems(): if section.startswith(section_filter): options.setdefault(section, {})[key] = option sections.setdefault(section, '') return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), tag.table(class_='wiki')(tag.tbody( tag.tr( tag.td(tag.tt(option.name)), tag.td( format_to_oneliner( self.env, formatter.context, dgettext(option.doc_domain, to_unicode(option.__doc__)))), tag.td( tag.code(option.default or 'false') if option.default or option.default is False else _("(no default)"), class_='default' if option.default or option.default is False else 'nodefault')) for option in sorted(options.get(section, {}).itervalues(), key=lambda o: o.name) if option.name.startswith(key_filter)))) for section, section_doc in sorted(sections.iteritems()))
def expand_macro(self, formatter, name, args): from trac.config import ConfigSection, Option section_filter = key_filter = '' args, kw = parse_args(args) if args: section_filter = args.pop(0).strip() if args: key_filter = args.pop(0).strip() registry = ConfigSection.get_registry(self.compmgr) sections = dict((name, dgettext(section.doc_domain, to_unicode(section.__doc__))) for name, section in registry.iteritems() if name.startswith(section_filter)) registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in registry.iteritems(): if section.startswith(section_filter): options.setdefault(section, {})[key] = option sections.setdefault(section, '') return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), tag.table(class_='wiki')(tag.tbody( tag.tr(tag.td(tag.tt(option.name)), tag.td(format_to_oneliner( self.env, formatter.context, dgettext(option.doc_domain, to_unicode(option.__doc__)))), tag.td(tag.code(option.default or 'false') if option.default or option.default is False else _("(no default)"), class_='default' if option.default or option.default is False else 'nodefault')) for option in sorted(options.get(section, {}).itervalues(), key=lambda o: o.name) if option.name.startswith(key_filter)))) for section, section_doc in sorted(sections.iteritems()))
class ExtraPermissionsProvider(Component): """Extra permission provider.""" implements(IPermissionRequestor) extra_permissions_section = ConfigSection( 'extra-permissions', doc="""This section provides a way to add arbitrary permissions to a Trac environment. This can be useful for adding new permissions to use for workflow actions, for example. To add new permissions, create a new section `[extra-permissions]` in your `trac.ini`. Every entry in that section defines a meta-permission and a comma-separated list of permissions. For example: {{{ [extra-permissions] extra_admin = extra_view, extra_modify, extra_delete }}} This entry will define three new permissions `EXTRA_VIEW`, `EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions `EXTRA_ADMIN` that grants all three permissions. If you don't want a meta-permission, start the meta-name with an underscore (`_`): {{{ [extra-permissions] _perms = extra_view, extra_modify }}} """) def get_permission_actions(self): permissions = {} for meta, perms in self.extra_permissions_section.options(): perms = [each.strip().upper() for each in perms.split(',')] for perm in perms: permissions.setdefault(perm, []) meta = meta.strip().upper() if meta and not meta.startswith('_'): permissions.setdefault(meta, []).extend(perms) return [(k, v) if v else k for k, v in permissions.iteritems()]
def render_admin_panel(self, req, cat, page, path_info): req.perm.require("TRAC_ADMIN") if path_info == None: ext = "" else: ext = "/" + path_info # # Gather section names for section drop down field # all_section_names = [] for section_name in self.config.sections(): if section_name == "components": continue all_section_names.append(section_name) # Check whether section exists and if it's not existing then check whether # its name is a valid section name. if ( (path_info is not None) and (path_info not in ("", "/", "_all_sections")) and (path_info not in all_section_names) ): if path_info == "components": add_warning(req, _('The section "components" can\'t be edited with the ini editor.')) req.redirect(req.href.admin(cat, page)) return None elif self.valid_section_name_chars_regexp.match(path_info) is None: add_warning(req, _("The section name %s is invalid.") % path_info) req.redirect(req.href.admin(cat, page)) return None # Add current section if it's not already in the list. This happens if # the section is essentially empty (i.e. newly created with no non-default # option values and no option from the option registry). all_section_names.append(path_info) registry = ConfigSection.get_registry(self.compmgr) descriptions = {} for section_name, section in registry.items(): if section_name == "components": continue doc = section.__doc__ if not section_name in all_section_names: all_section_names.append(section_name) if doc: descriptions[section_name] = dgettext(section.doc_domain, doc) all_section_names.sort() sections = {} # # Check security manager # manager = None try: manager = self.security_manager except Exception, detail: # "except ... as ..." is only available since Python 2.6 if req.method != "POST": # only add this warning once add_warning(req, _("Security manager could not be initated. %s") % unicode(detail))
class DefaultTicketGroupStatsProvider(Component): """Configurable ticket group statistics provider. See :teo:`TracIni#milestone-groups-section` for a detailed example configuration. """ implements(ITicketGroupStatsProvider) milestone_groups_section = ConfigSection( 'milestone-groups', """As the workflow for tickets is now configurable, there can be many ticket states, and simply displaying closed tickets vs. all the others is maybe not appropriate in all cases. This section enables one to easily create ''groups'' of states that will be shown in different colors in the milestone progress bar. Note that the groups can only be based on the ticket //status//, nothing else. In particular, it's not possible to distinguish between different closed tickets based on the //resolution//. Example configuration with three groups, //closed//, //new// and //active// (the default only has closed and active): {{{ # the 'closed' group correspond to the 'closed' tickets closed = closed # .order: sequence number in the progress bar closed.order = 0 # .query_args: optional parameters for the corresponding # query. In this example, the changes from the # default are two additional columns ('created' and # 'modified'), and sorting is done on 'created'. closed.query_args = group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime # .overall_completion: indicates groups that count for overall # completion percentage closed.overall_completion = true new = new new.order = 1 new.css_class = new new.label = new # Note: one catch-all group for other statuses is allowed active = * active.order = 2 # .css_class: CSS class for this interval active.css_class = open # .label: displayed label for this group active.label = in progress }}} The definition consists in a comma-separated list of accepted status. Also, '*' means any status and could be used to associate all remaining states to one catch-all group. The CSS class can be one of: new (yellow), open (no color) or closed (green). Other styles can easily be added using custom CSS rule: `table.progress td.<class> { background: <color> }` to a [TracInterfaceCustomization#SiteAppearance site/style.css] file for example. """) default_milestone_groups = [{ 'name': 'closed', 'status': 'closed', 'query_args': 'group=resolution', 'overall_completion': 'true' }, { 'name': 'active', 'status': '*', 'css_class': 'open' }] def _get_ticket_groups(self): """Returns a list of dict describing the ticket groups in the expected order of appearance in the milestone progress bars. """ if 'milestone-groups' in self.config: groups = {} order = 0 for groupname, value in self.milestone_groups_section.options(): qualifier = 'status' if '.' in groupname: groupname, qualifier = groupname.split('.', 1) group = groups.setdefault(groupname, { 'name': groupname, 'order': order }) group[qualifier] = value order = max(order, int(group['order'])) + 1 return [ group for group in sorted(groups.values(), key=lambda g: int(g['order'])) ] else: return self.default_milestone_groups def get_ticket_group_stats(self, ticket_ids): total_cnt = len(ticket_ids) all_statuses = set(TicketSystem(self.env).get_all_status()) status_cnt = {} for s in all_statuses: status_cnt[s] = 0 if total_cnt: for status, count in self.env.db_query(""" SELECT status, count(status) FROM ticket WHERE id IN (%s) GROUP BY status """ % ",".join(str(x) for x in sorted(ticket_ids))): status_cnt[status] = count stat = TicketGroupStats(_("ticket status"), _("tickets")) remaining_statuses = set(all_statuses) groups = self._get_ticket_groups() catch_all_group = None # we need to go through the groups twice, so that the catch up group # doesn't need to be the last one in the sequence for group in groups: status_str = group['status'].strip() if status_str == '*': if catch_all_group: raise TracError( _( "'%(group1)s' and '%(group2)s' milestone groups " "both are declared to be \"catch-all\" groups. " "Please check your configuration.", group1=group['name'], group2=catch_all_group['name'])) catch_all_group = group else: group_statuses = {s.strip() for s in status_str.split(',')} \ & all_statuses if group_statuses - remaining_statuses: raise TracError( _( "'%(groupname)s' milestone group reused status " "'%(status)s' already taken by other groups. " "Please check your configuration.", groupname=group['name'], status=', '.join(group_statuses - remaining_statuses))) else: remaining_statuses -= group_statuses group['statuses'] = group_statuses if catch_all_group: catch_all_group['statuses'] = remaining_statuses for group in groups: group_cnt = 0 query_args = {} for s, cnt in status_cnt.iteritems(): if s in group['statuses']: group_cnt += cnt query_args.setdefault('status', []).append(s) for arg in [ kv for kv in group.get('query_args', '').split(',') if '=' in kv ]: k, v = [a.strip() for a in arg.split('=', 1)] query_args.setdefault(k, []).append(v) stat.add_interval(group.get('label', group['name']), group_cnt, query_args, group.get('css_class', group['name']), as_bool(group.get('overall_completion'))) stat.refresh_calcs() return stat
class Environment(Component, ComponentManager): """Trac environment manager. Trac stores project information in a Trac environment. It consists of a directory structure containing among other things: * a configuration file, * project-specific templates and plugins, * the wiki and ticket attachments files, * the SQLite database file (stores tickets, wiki pages...) in case the database backend is sqlite """ implements(ISystemInfoProvider) required = True system_info_providers = ExtensionPoint(ISystemInfoProvider) setup_participants = ExtensionPoint(IEnvironmentSetupParticipant) components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component. The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module. Consider the following configuration snippet: {{{ [components] trac.ticket.report.ReportModule = disabled acct_mgr.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `acct_mgr` package. Note that the trailing wildcard is required for module/package matching. To view the list of active components, go to the ''Plugins'' page on ''About Trac'' (requires `CONFIG_VIEW` [wiki:TracPermissions permissions]). See also: TracPlugins """) shared_plugins_dir = PathOption( 'inherit', 'plugins_dir', '', """Path to the //shared plugins directory//. Plugins in that directory are loaded in addition to those in the directory of the environment `plugins`, with this one taking precedence. Non-absolute paths are relative to the Environment `conf` directory. """) base_url = Option( 'trac', 'base_url', '', """Reference URL for the Trac deployment. This is the base URL that will be used when producing documents that will be used outside of the web browsing context, like for example when inserting URLs pointing to Trac resources in notification e-mails.""") base_url_for_redirect = BoolOption( 'trac', 'use_base_url_for_redirect', False, """Optionally use `[trac] base_url` for redirects. In some configurations, usually involving running Trac behind a HTTP proxy, Trac can't automatically reconstruct the URL that is used to access it. You may need to use this option to force Trac to use the `base_url` setting also for redirects. This introduces the obvious limitation that this environment will only be usable when accessible from that URL, as redirects are frequently used. """) secure_cookies = BoolOption( 'trac', 'secure_cookies', False, """Restrict cookies to HTTPS connections. When true, set the `secure` flag on all cookies so that they are only sent to the server on HTTPS connections. Use this if your Trac instance is only accessible through HTTPS. """) anonymous_session_lifetime = IntOption( 'trac', 'anonymous_session_lifetime', '90', """Lifetime of the anonymous session, in days. Set the option to 0 to disable purging old anonymous sessions. (''since 1.0.17'')""") project_name = Option('project', 'name', 'My Project', """Name of the project.""") project_description = Option('project', 'descr', 'My example project', """Short description of the project.""") project_url = Option( 'project', 'url', '', """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails.""") project_admin = Option( 'project', 'admin', '', """E-Mail address of the project's administrator.""") project_admin_trac_url = Option( 'project', 'admin_trac_url', '.', """Base URL of a Trac instance where errors in this Trac should be reported. This can be an absolute or relative URL, or '.' to reference this Trac instance. An empty value will disable the reporting buttons. """) project_footer = Option( 'project', 'footer', N_('Visit the Trac open source project at<br />' '<a href="http://trac.edgewall.org/">' 'http://trac.edgewall.org/</a>'), """Page footer text (right-aligned).""") project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the project.""") log_type = ChoiceOption('logging', 'log_type', log.LOG_TYPES + log.LOG_TYPE_ALIASES, """Logging facility to use. Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""", case_sensitive=False) log_file = Option( 'logging', 'log_file', 'trac.log', """If `log_type` is `file`, this should be a path to the log-file. Relative paths are resolved relative to the `log` directory of the environment.""") log_level = ChoiceOption('logging', 'log_level', log.LOG_LEVELS + log.LOG_LEVEL_ALIASES, """Level of verbosity in log. Should be one of (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`). """, case_sensitive=False) log_format = Option( 'logging', 'log_format', None, """Custom logging format. If nothing is set, the following will be used: `Trac[$(module)s] $(levelname)s: $(message)s` In addition to regular key names supported by the [http://docs.python.org/library/logging.html Python logger library] one could use: - `$(path)s` the path for the current environment - `$(basename)s` the last path component of the current environment - `$(project)s` the project name Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the !ConfigParser itself. Example: `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s` """) def __init__(self, path, create=False, options=[]): """Initialize the Trac environment. :param path: the absolute path to the Trac environment :param create: if `True`, the environment is created and populated with default data; otherwise, the environment is expected to already exist. :param options: A list of `(section, name, value)` tuples that define configuration options """ ComponentManager.__init__(self) self.path = os.path.normpath(os.path.normcase(path)) self.log = None self.config = None if create: self.create(options) for setup_participant in self.setup_participants: setup_participant.environment_created() else: self.verify() self.setup_config() def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.path) @lazy def name(self): """The environment name. :since: 1.2 """ return os.path.basename(self.path) @property def env(self): """Property returning the `Environment` object, which is often required for functions and methods that take a `Component` instance. """ # The cached decorator requires the object have an `env` attribute. return self @property def system_info(self): """List of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. """ info = [] for provider in self.system_info_providers: info.extend(provider.get_system_info() or []) return sorted(set(info), key=lambda args: (args[0] != 'Trac', args[0].lower())) def get_systeminfo(self): """Return a list of `(name, version)` tuples describing the name and version information of external packages used by Trac and plugins. :since 1.3.1: deprecated and will be removed in 1.5.1. Use system_info property instead. """ return self.system_info # ISystemInfoProvider methods def get_system_info(self): yield 'Trac', self.trac_version yield 'Python', sys.version yield 'setuptools', setuptools.__version__ if pytz is not None: yield 'pytz', pytz.__version__ if hasattr(self, 'webfrontend_version'): yield self.webfrontend, self.webfrontend_version def component_activated(self, component): """Initialize additional member variables for components. Every component activated through the `Environment` object gets three member variables: `env` (the environment object), `config` (the environment configuration) and `log` (a logger object).""" component.env = self component.config = self.config component.log = self.log def _component_name(self, name_or_class): name = name_or_class if not isinstance(name_or_class, basestring): name = name_or_class.__module__ + '.' + name_or_class.__name__ return name.lower() @lazy def _component_rules(self): _rules = {} for name, value in self.components_section.options(): name = name.rstrip('.*').lower() _rules[name] = as_bool(value) return _rules def is_component_enabled(self, cls): """Implemented to only allow activation of components that are not disabled in the configuration. This is called by the `ComponentManager` base class when a component is about to be activated. If this method returns `False`, the component does not get activated. If it returns `None`, the component only gets activated if it is located in the `plugins` directory of the environment. """ component_name = self._component_name(cls) rules = self._component_rules cname = component_name while cname: enabled = rules.get(cname) if enabled is not None: return enabled idx = cname.rfind('.') if idx < 0: break cname = cname[:idx] # By default, all components in the trac package except # in trac.test or trac.tests are enabled return component_name.startswith('trac.') and \ not component_name.startswith('trac.test.') and \ not component_name.startswith('trac.tests.') or None def enable_component(self, cls): """Enable a component or module.""" self._component_rules[self._component_name(cls)] = True super(Environment, self).enable_component(cls) @contextmanager def component_guard(self, component, reraise=False): """Traps any runtime exception raised when working with a component and logs the error. :param component: the component responsible for any error that could happen inside the context :param reraise: if `True`, an error is logged but not suppressed. By default, errors are suppressed. """ try: yield except TracError as e: self.log.warning("Component %s failed with %s", component, exception_to_unicode(e)) if reraise: raise except Exception as e: self.log.error("Component %s failed with %s", component, exception_to_unicode(e, traceback=True)) if reraise: raise def verify(self): """Verify that the provided path points to a valid Trac environment directory.""" try: tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] if tag != _VERSION: raise Exception( _("Unknown Trac environment type '%(type)s'", type=tag)) except Exception as e: raise TracError( _("No Trac environment found at %(path)s\n" "%(e)s", path=self.path, e=e)) @lazy def db_exc(self): """Return an object (typically a module) containing all the backend-specific exception types as attributes, named according to the Python Database API (http://www.python.org/dev/peps/pep-0249/). To catch a database exception, use the following pattern:: try: with env.db_transaction as db: ... except env.db_exc.IntegrityError as e: ... """ return DatabaseManager(self).get_exceptions() @property def db_query(self): """Return a context manager (`~trac.db.api.QueryContextManager`) which can be used to obtain a read-only database connection. Example:: with env.db_query as db: cursor = db.cursor() cursor.execute("SELECT ...") for row in cursor.fetchall(): ... Note that a connection retrieved this way can be "called" directly in order to execute a query:: with env.db_query as db: for row in db("SELECT ..."): ... :warning: after a `with env.db_query as db` block, though the `db` variable is still defined, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... """ return QueryContextManager(self) @property def db_transaction(self): """Return a context manager (`~trac.db.api.TransactionContextManager`) which can be used to obtain a writable database connection. Example:: with env.db_transaction as db: cursor = db.cursor() cursor.execute("UPDATE ...") Upon successful exit of the context, the context manager will commit the transaction. In case of nested contexts, only the outermost context performs a commit. However, should an exception happen, any context manager will perform a rollback. You should *not* call `commit()` yourself within such block, as this will force a commit even if that transaction is part of a larger transaction. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") :warning: after a `with env.db_transaction` as db` block, though the `db` variable is still available, you shouldn't use it as it might have been closed when exiting the context, if this context was the outermost context (`db_query` or `db_transaction`). If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("UPDATE ...") """ return TransactionContextManager(self) def shutdown(self, tid=None): """Close the environment.""" from trac.versioncontrol.api import RepositoryManager RepositoryManager(self).shutdown(tid) DatabaseManager(self).shutdown(tid) if tid is None: log.shutdown(self.log) def create(self, options=[]): """Create the basic directory structure of the environment, initialize the database and populate the configuration file with default values. If options contains ('inherit', 'file'), default values will not be loaded; they are expected to be provided by that file or other options. :raises TracError: if the base directory of `path` does not exist. :raises TracError: if `path` exists and is not empty. """ base_dir = os.path.dirname(self.path) if not os.path.exists(base_dir): raise TracError( _( "Base directory '%(env)s' does not exist. Please create it " "and retry.", env=base_dir)) if os.path.exists(self.path) and os.listdir(self.path): raise TracError(_("Directory exists and is not empty.")) # Create the directory structure if not os.path.exists(self.path): os.mkdir(self.path) os.mkdir(self.htdocs_dir) os.mkdir(self.log_dir) os.mkdir(self.plugins_dir) os.mkdir(self.templates_dir) # Create a few files create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n') create_file( os.path.join(self.path, 'README'), 'This directory contains a Trac environment.\n' 'Visit http://trac.edgewall.org/ for more information.\n') # Setup the default configuration os.mkdir(self.conf_dir) config = Configuration(self.config_file_path) for section, name, value in options: config.set(section, name, value) config.save() self.setup_config() if not any((section, option) == ('inherit', 'file') for section, option, value in options): self.config.set_defaults(self) self.config.save() # Create the sample configuration create_file(self.config_file_path + '.sample') self._update_sample_config() # Create the database DatabaseManager(self).init_db() @lazy def database_version(self): """Returns the current version of the database. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('database_version') @lazy def database_initial_version(self): """Returns the version of the database at the time of creation. In practice, for database created before 0.11, this will return `False` which is "older" than any db version number. :since 1.0.2: """ return DatabaseManager(self) \ .get_database_version('initial_database_version') @lazy def trac_version(self): """Returns the version of Trac. :since: 1.2 """ from trac import core, __version__ return get_pkginfo(core).get('version', __version__) def setup_config(self): """Load the configuration file.""" self.config = Configuration(self.config_file_path, {'envname': self.name}) if not self.config.exists: raise TracError( _("The configuration file is not found at " "%(path)s", path=self.config_file_path)) self.setup_log() plugins_dir = self.shared_plugins_dir load_components(self, plugins_dir and (plugins_dir, )) @lazy def config_file_path(self): """Path of the trac.ini file.""" return os.path.join(self.conf_dir, 'trac.ini') @lazy def log_file_path(self): """Path to the log file.""" if not os.path.isabs(self.log_file): return os.path.join(self.log_dir, self.log_file) return self.log_file def _get_path_to_dir(self, *dirs): path = self.path for dir in dirs: path = os.path.join(path, dir) return os.path.realpath(path) @lazy def attachments_dir(self): """Absolute path to the attachments directory. :since: 1.3.1 """ return self._get_path_to_dir('files', 'attachments') @lazy def conf_dir(self): """Absolute path to the conf directory. :since: 1.0.11 """ return self._get_path_to_dir('conf') @lazy def files_dir(self): """Absolute path to the files directory. :since: 1.3.2 """ return self._get_path_to_dir('files') @lazy def htdocs_dir(self): """Absolute path to the htdocs directory. :since: 1.0.11 """ return self._get_path_to_dir('htdocs') @lazy def log_dir(self): """Absolute path to the log directory. :since: 1.0.11 """ return self._get_path_to_dir('log') @lazy def plugins_dir(self): """Absolute path to the plugins directory. :since: 1.0.11 """ return self._get_path_to_dir('plugins') @lazy def templates_dir(self): """Absolute path to the templates directory. :since: 1.0.11 """ return self._get_path_to_dir('templates') def setup_log(self): """Initialize the logging sub-system.""" self.log, log_handler = \ self.create_logger(self.log_type, self.log_file_path, self.log_level, self.log_format) self.log.addHandler(log_handler) self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32, self.trac_version) def create_logger(self, log_type, log_file, log_level, log_format): log_id = 'Trac.%s' % hashlib.sha1(self.path).hexdigest() if log_format: log_format = log_format.replace('$(', '%(') \ .replace('%(path)s', self.path) \ .replace('%(basename)s', self.name) \ .replace('%(project)s', self.project_name) return log.logger_handler_factory(log_type, log_file, log_level, log_id, format=log_format) def get_known_users(self, as_dict=False): """Returns information about all known users, i.e. users that have logged in to this Trac environment and possibly set their name and email. By default this function returns a iterator that yields one tuple for every user, of the form (username, name, email), ordered alpha-numerically by username. When `as_dict` is `True` the function returns a dictionary mapping username to a (name, email) tuple. :since 1.2: the `as_dict` parameter is available. """ return self._known_users_dict if as_dict else iter(self._known_users) @cached def _known_users(self): return self.db_query(""" SELECT DISTINCT s.sid, n.value, e.value FROM session AS s LEFT JOIN session_attribute AS n ON (n.sid=s.sid AND n.authenticated=1 AND n.name = 'name') LEFT JOIN session_attribute AS e ON (e.sid=s.sid AND e.authenticated=1 AND e.name = 'email') WHERE s.authenticated=1 ORDER BY s.sid """) @cached def _known_users_dict(self): return {u[0]: (u[1], u[2]) for u in self._known_users} def invalidate_known_users_cache(self): """Clear the known_users cache.""" del self._known_users del self._known_users_dict def backup(self, dest=None): """Create a backup of the database. :param dest: Destination file; if not specified, the backup is stored in a file called db_name.trac_version.bak """ return DatabaseManager(self).backup(dest) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): self.log.warning( "Component %s requires environment upgrade", participant) return True return False def upgrade(self, backup=False, backup_dest=None): """Upgrade database. :param backup: whether or not to backup before upgrading :param backup_dest: name of the backup file :return: whether the upgrade was performed """ upgraders = [] for participant in self.setup_participants: with self.component_guard(participant, reraise=True): if participant.environment_needs_upgrade(): upgraders.append(participant) if not upgraders: return if backup: try: self.backup(backup_dest) except Exception as e: raise BackupError(e) for participant in upgraders: self.log.info("upgrading %s...", participant) with self.component_guard(participant, reraise=True): participant.upgrade_environment() # Database schema may have changed, so close all connections dbm = DatabaseManager(self) if dbm.connection_uri != 'sqlite::memory:': dbm.shutdown() self._update_sample_config() del self.database_version return True @lazy def href(self): """The application root path""" return Href(urlsplit(self.abs_href.base).path) @lazy def abs_href(self): """The application URL""" if not self.base_url: self.log.warning("base_url option not set in configuration, " "generated links may be incorrect") return Href(self.base_url) def _update_sample_config(self): filename = os.path.join(self.config_file_path + '.sample') if not os.path.isfile(filename): return config = Configuration(filename) config.set_defaults() try: config.save() except EnvironmentError as e: self.log.warning("Couldn't write sample configuration file (%s)%s", e, exception_to_unicode(e, traceback=True)) else: self.log.info( "Wrote sample configuration file with the new " "settings and their default values: %s", filename)
class InterWikiMap(Component): """InterWiki map manager.""" implements(IWikiChangeListener, IWikiMacroProvider) interwiki_section = ConfigSection( 'interwiki', """Every option in the `[interwiki]` section defines one InterWiki prefix. The option name defines the prefix. The option value defines the URL, optionally followed by a description separated from the URL by whitespace. Parametric URLs are supported as well. '''Example:''' {{{ [interwiki] MeatBall = http://www.usemod.com/cgi-bin/mb.pl? PEP = http://www.python.org/peps/pep-$1.html Python Enhancement Proposal $1 tsvn = tsvn: Interact with TortoiseSvn }}} """) _page_name = 'InterMapTxt' _interwiki_re = re.compile( r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" % WikiParser.LINK_SCHEME, re.UNICODE) _argspec_re = re.compile(r"\$\d") # The component itself behaves as a read-only map def __contains__(self, ns): return ns.upper() in self.interwiki_map def __getitem__(self, ns): return self.interwiki_map[ns.upper()] def keys(self): return list(self.interwiki_map) # Expansion of positional arguments ($1, $2, ...) in URL and title def _expand(self, txt, args): """Replace "$1" by the first args, "$2" by the second, etc.""" def setarg(match): num = int(match.group()[1:]) return args[num - 1] if 0 < num <= len(args) else '' return re.sub(InterWikiMap._argspec_re, setarg, txt) def _expand_or_append(self, txt, args): """Like expand, but also append first arg if there's no "$".""" if not args: return txt expanded = self._expand(txt, args) return txt + args[0] if expanded == txt else expanded def url(self, ns, target): """Return `(url, title)` for the given InterWiki `ns`. Expand the colon-separated `target` arguments. """ ns, url, title = self[ns] maxargnum = max( [0] + [int(a[1:]) for a in re.findall(InterWikiMap._argspec_re, url)]) target, query, fragment = split_url_into_path_query_fragment(target) if maxargnum > 0: args = target.split(':', (maxargnum - 1)) else: args = [target] url = self._expand_or_append(url, args) ntarget, nquery, nfragment = split_url_into_path_query_fragment(url) if query and nquery: nquery = '%s&%s' % (nquery, query[1:]) else: nquery = nquery or query nfragment = fragment or nfragment # user provided takes precedence expanded_url = ntarget + nquery + nfragment if not self._is_safe_url(expanded_url): expanded_url = '' expanded_title = self._expand(title, args) if expanded_title == title: expanded_title = _("%(target)s in %(name)s", target=target, name=title) return expanded_url, expanded_title # IWikiChangeListener methods def wiki_page_added(self, page): if page.name == InterWikiMap._page_name: del self.interwiki_map def wiki_page_changed(self, page, version, t, comment, author): if page.name == InterWikiMap._page_name: del self.interwiki_map def wiki_page_deleted(self, page): if page.name == InterWikiMap._page_name: del self.interwiki_map def wiki_page_version_deleted(self, page): if page.name == InterWikiMap._page_name: del self.interwiki_map @cached def interwiki_map(self): """Map from upper-cased namespaces to (namespace, prefix, title) values. """ from trac.wiki.model import WikiPage map = {} content = WikiPage(self.env, InterWikiMap._page_name).text in_map = False for line in content.split('\n'): if in_map: if line.startswith('----'): in_map = False else: m = re.match(InterWikiMap._interwiki_re, line) if m: prefix, url, title = m.groups() url = url.strip() title = title.strip() if title else prefix map[prefix.upper()] = (prefix, url, title) elif line.startswith('----'): in_map = True for prefix, value in self.interwiki_section.options(): value = value.split(None, 1) if value: url = value[0].strip() title = value[1].strip() if len(value) > 1 else prefix map[prefix.upper()] = (prefix, url, title) return map # IWikiMacroProvider methods def get_macros(self): yield 'InterWiki' def get_macro_description(self, name): return 'messages', \ N_("Provide a description list for the known InterWiki " "prefixes.") def expand_macro(self, formatter, name, content): interwikis = [] for k in sorted(self.keys()): prefix, url, title = self[k] interwikis.append({ 'prefix': prefix, 'url': url, 'title': title, 'rc_url': self._expand_or_append(url, ['RecentChanges']), 'description': url if title == prefix else title }) return tag.table(tag.tr(tag.th(tag.em( _("Prefix"))), tag.th(tag.em(_("Site")))), [ tag.tr(tag.td(tag.a(w['prefix'], href=w['rc_url'])), tag.td(tag.a(w['description'], href=w['url']))) for w in interwikis ], class_="wiki interwiki") # Internal methods def _is_safe_url(self, url): return WikiSystem(self.env).render_unsafe_content or \ ':' not in url or \ url.split(':', 1)[0] in self._safe_schemes @lazy def _safe_schemes(self): return set(WikiSystem(self.env).safe_schemes)
class SubversionPropertyRenderer(Component): implements(IPropertyRenderer) svn_externals_section = ConfigSection( 'svn:externals', """The TracBrowser for Subversion can interpret the `svn:externals` property of folders. By default, it only turns the URLs into links as Trac can't browse remote repositories. However, if you have another Trac instance (or an other repository browser like [http://www.viewvc.org/ ViewVC]) configured to browse the target repository, then you can instruct Trac which other repository browser to use for which external URL. This mapping is done in the `[svn:externals]` section of the TracIni. Example: {{{ [svn:externals] 1 = svn://server/repos1 http://trac/proj1/browser/$path?rev=$rev 2 = svn://server/repos2 http://trac/proj2/browser/$path?rev=$rev 3 = http://theirserver.org/svn/eng-soft http://ourserver/viewvc/svn/$path/?pathrev=25914 4 = svn://anotherserver.com/tools_repository http://ourserver/tracs/tools/browser/$path?rev=$rev }}} With the above, the `svn://anotherserver.com/tools_repository/tags/1.1/tools` external will be mapped to `http://ourserver/tracs/tools/browser/tags/1.1/tools?rev=` (and `rev` will be set to the appropriate revision number if the external additionally specifies a revision, see the [%(svnbook)s SVN Book on externals] for more details). Note that the number used as a key in the above section is purely used as a place holder, as the URLs themselves can't be used as a key due to various limitations in the configuration file parser. Finally, the relative URLs introduced in [http://subversion.apache.org/docs/release-notes/1.5.html#externals Subversion 1.5] are not yet supported. """, doc_args={ 'svnbook': 'http://svnbook.red-bean.com/en/1.7/svn.advanced.externals.html' }) def __init__(self): self._externals_map = {} # IPropertyRenderer methods def match_property(self, name, mode): if name in ('svn:externals', 'svn:needs-lock'): return 4 return 2 if name in ('svn:mergeinfo', 'svnmerge-blocked', 'svnmerge-integrated') else 0 def render_property(self, name, mode, context, props): if name == 'svn:externals': return self._render_externals(props[name]) elif name == 'svn:needs-lock': return self._render_needslock(context) elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): return self._render_mergeinfo(name, mode, context, props) def _is_abs_url(self, url): return url and '://' in url def _render_externals(self, prop): if not self._externals_map: for dummykey, value in self.svn_externals_section.options(): value = value.split() if len(value) != 2: self.log.warning( "svn:externals entry %s doesn't contain " "a space-separated key value pair, " "skipping.", dummykey) continue key, value = value self._externals_map[key] = value.replace('%', '%%') \ .replace('$path', '%(path)s') \ .replace('$rev', '%(rev)s') externals = [] for external in prop.splitlines(): elements = external.split() if not elements: continue localpath, rev, url = elements[0], '', elements[-1] if localpath.startswith('#'): externals.append((external, None, None, None, None)) continue if len(elements) == 3: rev = elements[1] rev = rev.replace('-r', '') # retrieve a matching entry in the externals map if not self._is_abs_url(url): externals.append((external, None, None, None, None)) continue prefix = [] base_url = url while base_url: if base_url in self._externals_map or base_url == u'/': break base_url, pref = posixpath.split(base_url) prefix.append(pref) href = self._externals_map.get(base_url) revstr = ' at revision ' + rev if rev else '' if not href and (url.startswith('http://') or url.startswith('https://')): href = url.replace('%', '%%') if href: remotepath = '' if prefix: remotepath = posixpath.join(*reversed(prefix)) externals.append( (localpath, revstr, base_url, remotepath, href % { 'path': remotepath, 'rev': rev })) else: externals.append((localpath, revstr, url, None, None)) externals_data = [] for localpath, rev, url, remotepath, href in externals: label = localpath if url is None: title = '' elif href: if url: url = ' in ' + url label += rev + url title = ''.join((remotepath, rev, url)) else: title = _('No svn:externals configured in trac.ini') externals_data.append((label, href, title)) return tag.ul([ tag.li(tag.a(label, href=href, title=title)) for label, href, title in externals_data ]) def _render_needslock(self, context): url = chrome_resource_path(context.req, 'common/lock-locked.png') return tag.img(src=url, alt=_("needs lock"), title=_("needs lock")) def _render_mergeinfo(self, name, mode, context, props): rows = [] for row in props[name].splitlines(): try: (path, revs) = row.rsplit(':', 1) rows.append( [tag.td(path), tag.td(revs.replace(',', u',\u200b'))]) except ValueError: rows.append(tag.td(row, colspan=2)) return tag.table(tag.tbody([tag.tr(row) for row in rows]), class_='props')
class Phrases(Component): """Highlight attentional phrases like `FIXME`. Phrases that are highlighted are defined in the `[wikiextras]` section in `trac.ini`. Use the `ShowPhrases` macro to show a list of currently defined phrases. """ implements(IRequestFilter, ITemplateProvider, IWikiSyntaxProvider, IWikiMacroProvider) fixme_phrases = ListOption('wikiextras', 'fixme_phrases', 'BUG, FIXME', doc= """A list of attentional phrases or single words, separated by comma (`,`) that will be highlighted to catch attention. Any delimiter `():<>` adjacent to a phrase will not be presented. (i.e. do not include any of these delimiters in this list). This makes it possible to naturally write, for example, `FIXME:` in a wiki text, but view the phrase highlighted without the colon (`:`) which would not look natural. Use the `ShowPhrases` macro to show a list of currently defined phrases.""") todo_phrases = ListOption('wikiextras', 'todo_phrases', 'REVIEW, TODO', doc="Analogous to `FIXME`-phrases, but " "presentation is less eye-catching.") done_phrases = ListOption('wikiextras', 'done_phrases', 'DONE, DEBUGGED, FIXED, REVIEWED', doc="Analogous to `FIXME`-phrases, but " "presentation is less eye-catching.") custom_phrases_section = ConfigSection('wikiextras-custom-phrases', """Custom phrases are configurable by providing associations between a CSS class and the list of phrases separated by comma. Example: {{{#!ini [wikiextras-custom-phrases] nice = NICE, COOL }}} """) def __init__(self): self.text = {} #noinspection PyArgumentList html_form = '<span class="wikiextras phrase %s">%s</span>' def add_style(style, phrases): for phrase in phrases: html = html_form % (style, phrase) self.text[phrase] = html for (d1, d2) in [(':', ':'), ('<', '>'), ('(', ')')]: self.text['%s%s%s' % (d1, phrase, d2)] = html for d2 in [':']: self.text['%s%s' % (phrase, d2)] = html for style, phrases in [('fixme', self.fixme_phrases), ('todo', self.todo_phrases), ('done', self.done_phrases)]: add_style(style, phrases) for style, phrases in self.custom_phrases_section.options(): add_style(style, phrases.split(',')) # IRequestFilter methods #noinspection PyUnusedLocal def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): add_stylesheet(req, 'wikiextras/css/phrases.css') return template, data, content_type # ITemplateProvider methods def get_htdocs_dirs(self): return [('wikiextras', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return [] # IWikiSyntaxProvider methods def get_wiki_syntax(self): yield ('!?(?:%s)' % prepare_regexp(self.text), self._format_phrase) def get_link_resolvers(self): return [] #noinspection PyUnusedLocal def _format_phrase(self, formatter, match, fullmatch): return Markup(self.text[match]) # IWikiMacroProvider methods def get_macros(self): yield 'ShowPhrases' #noinspection PyUnusedLocal def get_macro_description(self, name): return cleandoc("""Renders in a table the list of known phrases that are highlighted to catch attention. Comment: Any delimiter `():<>` adjacent to a phrase will not be presented. This makes it possible to naturally write `FIXME:`, for example, but view the phrase highlighted without the colon (`:`) which would not look natural. Prefixing a phrase with `!` prevents it from being highlighted. """) #noinspection PyUnusedLocal def expand_macro(self, formatter, name, content, args=None): t = [render_table(p, '1', lambda s: self._format_phrase(formatter, s, None)) for p in [self.fixme_phrases, self.todo_phrases, self.done_phrases]] style = 'border:none;text-align:center;vertical-align:top' spacer = tag.td(style='width:2em;border:none') return tag.table(tag.tr(tag.td(t[0], style=style), spacer, tag.td(t[1], style=style), spacer, tag.td(t[2], style=style)))
class ConfigurableTicketWorkflow(Component): """Ticket action controller which provides actions according to a workflow defined in trac.ini. The workflow is defined in the `[ticket-workflow]` section of the [wiki:TracIni#ticket-workflow-section trac.ini] configuration file. """ implements(IEnvironmentSetupParticipant, ITicketActionController) ticket_workflow_section = ConfigSection('ticket-workflow', """The workflow for tickets is controlled by plugins. By default, there's only a `ConfigurableTicketWorkflow` component in charge. That component allows the workflow to be configured via this section in the `trac.ini` file. See TracWorkflow for more details. (''since 0.11'')""") def __init__(self, *args, **kwargs): self.actions = self.get_all_actions() self.log.debug('Workflow actions at initialization: %s\n', self.actions) # IEnvironmentSetupParticipant methods def environment_created(self): """When an environment is created, we provide the basic-workflow, unless a ticket-workflow section already exists. """ if 'ticket-workflow' not in self.config.sections(): load_workflow_config_snippet(self.config, 'basic-workflow.ini') self.config.save() self.actions = self.get_all_actions() def environment_needs_upgrade(self, db): """The environment needs an upgrade if there is no [ticket-workflow] section in the config. """ return not list(self.config.options('ticket-workflow')) def upgrade_environment(self, db): """Insert a [ticket-workflow] section using the original-workflow""" load_workflow_config_snippet(self.config, 'original-workflow.ini') self.config.save() self.actions = self.get_all_actions() info_message = """ ==== Upgrade Notice ==== The ticket Workflow is now configurable. Your environment has been upgraded, but configured to use the original workflow. It is recommended that you look at changing this configuration to use basic-workflow. Read TracWorkflow for more information (don't forget to 'wiki upgrade' as well) """ self.log.info(info_message.replace('\n', ' ').replace('==', '')) print info_message # ITicketActionController methods def get_ticket_actions(self, req, ticket): """Returns a list of (weight, action) tuples that are valid for this request and this ticket.""" # Get the list of actions that can be performed # Determine the current status of this ticket. If this ticket is in # the process of being modified, we need to base our information on the # pre-modified state so that we don't try to do two (or more!) steps at # once and get really confused. status = ticket._old.get('status', ticket['status']) or 'new' ticket_perm = req.perm(ticket.resource) allowed_actions = [] for action_name, action_info in self.actions.items(): oldstates = action_info['oldstates'] if oldstates == ['*'] or status in oldstates: # This action is valid in this state. Check permissions. required_perms = action_info['permissions'] if self._is_action_allowed(ticket_perm, required_perms): allowed_actions.append((action_info['default'], action_name)) # Append special `_reset` action if status is invalid. if status not in TicketSystem(self.env).get_all_status() + \ ['new', 'closed']: required_perms = self.actions['_reset'].get('permissions') if self._is_action_allowed(ticket_perm, required_perms): default = self.actions['_reset'].get('default') allowed_actions.append((default, '_reset')) return allowed_actions def _is_action_allowed(self, ticket_perm, required_perms): if not required_perms: return True for permission in required_perms: if permission in ticket_perm: return True return False def get_all_status(self): """Return a list of all states described by the configuration. """ all_status = set() for attributes in self.actions.values(): all_status.update(attributes['oldstates']) all_status.add(attributes['newstate']) all_status.discard('*') all_status.discard('') return all_status def render_ticket_action_control(self, req, ticket, action): self.log.debug('render_ticket_action_control: action "%s"', action) this_action = self.actions[action] status = this_action['newstate'] operations = this_action['operations'] current_owner = ticket._old.get('owner', ticket['owner']) author = get_reporter_id(req, 'author') format_author = partial(Chrome(self.env).format_author, req) formatted_current_owner = format_author(current_owner or _("(none)")) control = [] # default to nothing hints = [] if 'reset_workflow' in operations: control.append(_("from invalid state")) hints.append(_("Current state no longer exists")) if 'del_owner' in operations: hints.append(_("The ticket will be disowned")) if 'set_owner' in operations: id = 'action_%s_reassign_owner' % action if 'set_owner' in this_action: owners = [x.strip() for x in this_action['set_owner'].split(',')] elif self.config.getbool('ticket', 'restrict_owner'): perm = PermissionSystem(self.env) owners = perm.get_users_with_permission('TICKET_MODIFY') owners.sort() else: owners = None if owners is None: owner = req.args.get(id, author) control.append(tag_("to %(owner)s", owner=tag.input(type='text', id=id, name=id, value=owner))) hints.append(_("The owner will be changed from " "%(current_owner)s to the specified user", current_owner=formatted_current_owner)) elif len(owners) == 1: owner = tag.input(type='hidden', id=id, name=id, value=owners[0]) formatted_new_owner = format_author(owners[0]) control.append(tag_("to %(owner)s", owner=tag(formatted_new_owner, owner))) if ticket['owner'] != owners[0]: hints.append(_("The owner will be changed from " "%(current_owner)s to %(selected_owner)s", current_owner=formatted_current_owner, selected_owner=formatted_new_owner)) else: selected_owner = req.args.get(id, req.authname) control.append(tag_("to %(owner)s", owner=tag.select( [tag.option(x, value=x, selected=(x == selected_owner or None)) for x in owners], id=id, name=id))) hints.append(_("The owner will be changed from " "%(current_owner)s to the selected user", current_owner=formatted_current_owner)) elif 'set_owner_to_self' in operations and \ ticket._old.get('owner', ticket['owner']) != author: hints.append(_("The owner will be changed from %(current_owner)s " "to %(authname)s", current_owner=formatted_current_owner, authname=format_author(author))) if 'set_resolution' in operations: if 'set_resolution' in this_action: resolutions = [x.strip() for x in this_action['set_resolution'].split(',')] else: resolutions = [r.name for r in Resolution.select(self.env)] if not resolutions: raise TracError(_("Your workflow attempts to set a resolution " "but none is defined (configuration issue, " "please contact your Trac admin).")) id = 'action_%s_resolve_resolution' % action if len(resolutions) == 1: resolution = tag.input(type='hidden', id=id, name=id, value=resolutions[0]) control.append(tag_("as %(resolution)s", resolution=tag(resolutions[0], resolution))) hints.append(_("The resolution will be set to %(name)s", name=resolutions[0])) else: selected_option = req.args.get(id, TicketSystem(self.env).default_resolution) control.append(tag_("as %(resolution)s", resolution=tag.select( [tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions], id=id, name=id))) hints.append(_("The resolution will be set")) if 'del_resolution' in operations: hints.append(_("The resolution will be deleted")) if 'leave_status' in operations: control.append(_("as %(status)s", status= ticket._old.get('status', ticket['status']))) if len(operations) == 1: hints.append(_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner) if current_owner else _("The ticket will remain with no owner")) else: if status != '*': hints.append(_("Next status will be '%(name)s'", name=status)) return (this_action.get('name', action), tag(separated(control, ' ')), '. '.join(hints) + '.' if hints else '') def get_ticket_changes(self, req, ticket, action): this_action = self.actions[action] # Enforce permissions if not self._has_perms_for_action(req, this_action, ticket.resource): # The user does not have any of the listed permissions, so we won't # do anything. return {} updated = {} # Status changes status = this_action['newstate'] if status != '*': updated['status'] = status for operation in this_action['operations']: if operation == 'del_owner': updated['owner'] = '' elif operation == 'set_owner': newowner = req.args.get('action_%s_reassign_owner' % action, this_action.get('set_owner', '').strip()) # If there was already an owner, we get a list, [new, old], # but if there wasn't we just get new. if type(newowner) == list: newowner = newowner[0] updated['owner'] = newowner elif operation == 'set_owner_to_self': updated['owner'] = get_reporter_id(req, 'author') elif operation == 'del_resolution': updated['resolution'] = '' elif operation == 'set_resolution': newresolution = req.args.get('action_%s_resolve_resolution' % \ action, this_action.get('set_resolution', '').strip()) updated['resolution'] = newresolution # reset_workflow is just a no-op here, so we don't look for it. # leave_status is just a no-op here, so we don't look for it. return updated def apply_action_side_effects(self, req, ticket, action): pass def _has_perms_for_action(self, req, action, resource): required_perms = action['permissions'] if required_perms: for permission in required_perms: if permission in req.perm(resource): break else: # The user does not have any of the listed permissions return False return True # Public methods (for other ITicketActionControllers that want to use # our config file and provide an operation for an action) def get_all_actions(self): actions = parse_workflow_config(self.ticket_workflow_section.options()) # Special action that gets enabled if the current status no longer # exists, as no other action can then change its state. (#5307/#11850) if '_reset' not in actions: reset = { 'default': 0, 'name': 'reset', 'newstate': 'new', 'oldstates': [], 'operations': ['reset_workflow'], 'permissions': ['TICKET_ADMIN'] } for key, val in reset.items(): actions['_reset'][key] = val for name, info in actions.iteritems(): if not info['newstate']: self.log.warning("Ticket workflow action '%s' doesn't define " "any transitions", name) return actions def get_actions_by_operation(self, operation): """Return a list of all actions with a given operation (for use in the controller's get_all_status()) """ actions = [(info['default'], action) for action, info in self.actions.items() if operation in info['operations']] return actions def get_actions_by_operation_for_req(self, req, ticket, operation): """Return list of all actions with a given operation that are valid in the given state for the controller's get_ticket_actions(). If state='*' (the default), all actions with the given operation are returned. """ # Be sure to look at the original status. status = ticket._old.get('status', ticket['status']) actions = [(info['default'], action) for action, info in self.actions.items() if operation in info['operations'] and ('*' in info['oldstates'] or status in info['oldstates']) and self._has_perms_for_action(req, info, ticket.resource)] return actions
class WorkflowManager(Component): config_section = ConfigSection('ticket-workflow-action-buttons', '') @property def action_controllers(self): return TicketSystem(self.env).action_controllers def allowed_actions(self, allowed, req, ticket): return [ action for action in TicketSystem(self.env).get_available_actions( req, ticket) if allowed is None or action in allowed ] def controllers_for_action(self, req, ticket, action): return [ controller for controller in self.action_controllers if action in [i[1] for i in controller.get_ticket_actions(req, ticket)] ] def render_action_control(self, req, ticket, action): first_label = None widgets = [] hints = [] for controller in self.controllers_for_action(req, ticket, action): print controller, action label, widget, hint = controller.render_ticket_action_control( req, ticket, action) if first_label is None: first_label = label widgets.append(widget) hints.append(hint) return first_label, tag(*widgets), (hints and '. '.join(hints) or '') _default_icons = { "accept": "fa-thumbs-o-up", "leave": "fa-comments-o", "reassign": "fa-random", "reopen": "fa-minus-square-o", "resolve": "fa-check-square-o", } def render_action_button(self, req, ticket, action): template = """ <label class="button" style="%(css)s"> <input type="hidden" name="action" value="%(action)s" /> <a %(comment_required)s name="act"><i class='fa %(icon)s'></i> %(title)s</a> """ data = { "action": action, "css": self.config_section.get("%s.css" % action) or "", "comment_required": (self.config_section.get("%s.comment" % action) == "required" and 'data-comment="required"' or ""), "icon": self.config_section.get("%s.icon" % action, self._default_icons.get(action)), "title": self.config_section.get("%s.title" % action, action.title()), } markup = template % data supplemental_form = "" label, widgets, hints = self.render_action_control(req, ticket, action) if widgets.children: supplemental_form = "<div class='supplemental'><div class='supplemental-form'>%s %s <span class='hint'>%s</span><textarea style='width:95%%' rows='5' name='comment' placeholder='Enter your comment'></textarea><input type='submit' /></div></div>" % ( action.title(), str(widgets), hints) markup = markup + supplemental_form + "</label>" return Markup(markup)
class ConfigurableCommitTicketReferenceMacro(CommitTicketReferenceMacro): """ An extension of Trac CommitTicketUpdater's CommitTicketReferenceMacro that does not search for occurrences of the ticket reference in a referenced commit's message. This avoids the dependency on CommitTicketUpdater. """ # pylint: disable=abstract-method ticket_replace_section_name = 'commit-ticket-update-replace' ticket_replace_section = ConfigSection( ticket_replace_section_name, """In this section, you can define patterns for substitution in the commit message in the format: name.pattern = PR-\d+ name.replace = https://example.org/$(repository)s/\1 The name has no further meaning than identifying a pair of pattern and replace and will be ignored. The following variables will be substituted in both pattern and replace before applying the regular expression: - $(repository)s name of the repository committed to - $(revision)s revision of the commit Note the usage of `$(...)s` instead of `%(...)s` as the latter form would be interpreted by the ConfigParser itself. """) def expand_macro(self, formatter, name, content, args=None): # pylint: disable=too-many-function-args args = args or {} reponame = args.get('repository') or '' rev = args.get('revision') # pylint: disable=no-member repos = RepositoryManager(self.env).get_repository(reponame) try: changeset = repos.get_changeset(rev) message = changeset.message rev = changeset.rev resource = repos.resource except Exception: # pylint: disable=broad-except message = content resource = Resource('repository', reponame) config = self.ticket_replace_section fields = {} for key, value in config.options(): idx = key.rfind('.') if idx >= 0: prefix, attribute = key[:idx], key[idx + 1:] field = fields.setdefault(prefix, {}) field[attribute] = config.get(key) else: fields[key] = {'': value} for prefix, field in fields.iteritems(): if not all(k in field for k in ['pattern', 'replace']): self.log.warn( "Ignoring [%s] %s, missing .pattern or .replace" % (self.ticket_replace_section_name, key)) continue subst = {'repository': reponame, 'revision': rev} pattern = field['pattern'].replace('$(', '%(') % subst replace = field['replace'].replace('$(', '%(') % subst message = re.sub(pattern, replace, message) if ChangesetModule(self.env).wiki_format_messages: message = '\n'.join( map(lambda line: "> " + line, message.split('\n'))) return tag.div(format_to_html(self.env, formatter.context.child( 'changeset', rev, parent=resource), message, escape_newlines=True), class_='message') else: return tag.pre(message, class_='message')
class PasteParser(Component): implements(IRequestFilter, ITemplateProvider) paste_parser_section = ConfigSection('paste-parser', """ When text is pasted into the designated field of a new ticket this plugin can parse that pasted text and populate the results into other ticket fields as defined by these configuration options. This is useful where ticket information is received via email in a very structured format that can be parsed. Users can copy-paste the email into one text field of a new ticket and then that pasted text will instantly be parsed and populated into the appropriate ticket fields for the user to review and adjust as needed before saving. """) paste_parser_xref_section = ConfigSection('paste-parser-xref', """ These section contains all the information related to the fields that should be searched for in the pasted text. """) pasted_text_pattern = Option('paste-parser', 'pasted_text_pattern', doc="""This is the regular expression pattern that this plugin will attempt to match against the text pasted into the designated field of a new ticket. If this pattern does not find a match within the pasted text, then nothing happens. If this pattern matches all or part of what was pasted into the designated field of a new ticket, then this plugin will immediately attempt to parse the matched text based on the the definitions in this config and populate the resulting values into the related ticket fields.""") field_not_found_for_updating_label = Option('paste-parser', 'field_not_found_for_updating_label', doc="""Label inserted into the paste-field to identify fields that could not be found in the DOM to be updated.""") invalid_field_values_not_in_list_label = Option('paste-parser', 'invalid_field_values_not_in_list_label', doc="""Label inserted into the paste-field to identify field values that could not be assigned because the value is not in the defined SELECT list of OPTIONS.""") key_value_delimiter = Option('paste-parser', 'key_value_delimiter', doc="""This is the delimiter that will be appended to all given 'source_key' strings (the source line attribute label) when attempting to match field labels in the pasted designated field text.""") ignore_pattern = Option('paste-parser', 'ignore_pattern', doc="""This is the regular expression that defines which strings within the pasted text should be ignored/skipped and not be parsed for key-values These strings are stripped out of the input string prior to processing.""") field_to_parse = Option('paste-parser', 'field_to_parse', doc="""This is the name of the ticket field to be parsed. To get the actual DOM element for this field, this field name will be converted to the ID of the field's DOM element via a regexp replace with the field_name_to_id_match as the match pattern and the field_name_to_id_replace as the replacement pattern.""") field_name_to_id_match = Option('paste-parser', 'field_name_to_id_match', doc="""This is a regexp matching pattern that will be used to match against the designated field name and then the field_name_to_id_replace will be used as the replacement pattern to convert the field name to an ID so that we can get the actual DOM element for this field.""") field_name_to_id_replace = Option('paste-parser', 'field_name_to_id_replace', doc="""This is a regexp replacement pattern that will be used in conjunction with the field_name_to_id_match to convert the field name to an ID so that we can get the actual DOM element for this field.""") key_value_end_pattern = Option('paste-parser', 'key_value_end_pattern', doc="""This optional regexp matching pattern defines the end of each key/value pair. If not given, a new line determines the end. This is useful if values span multiple lines.""") debug_on = Option('paste-parser', 'debug_on', doc="""If set to true, extensive debugging will be sent to the browser's console.""") def _get_xref(self): """Returns a list of dict describing the config options from trac.ini that define how to parse the pasted designated field text. Based on _get_ticket_groups() from Trac v1.2 trac/ticket/roadmap.py """ if 'paste-parser-xref' in self.config: xrefs = {} order = 0 for field_name, value in self.paste_parser_xref_section.options(): qualifier = 'regexp' if '.' in field_name: field_name, qualifier = field_name.split('.', 1) self.log.debug("[PasteParser] field_name=%s qualifier=%s", field_name, qualifier) field = xrefs.setdefault(field_name, {'name': field_name, 'order': order}) self.log.debug('[PasteParser] json.dumps(field) PRE='+json.dumps(field)) field[qualifier] = value self.log.debug('[PasteParser] json.dumps(field) POST='+json.dumps(field)) order = max(order, int(field['order'])) + 1 self.log.debug('[PasteParser] json.dumps(xrefs)='+json.dumps(xrefs)) return [field for field in sorted(xrefs.values(), key=lambda g: int(g['order']))] else: return None # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if template == 'ticket.html' and req.path_info == '/newticket': add_script(req, 'PasteParser/js/PasteParser.js') paste_parser_config = { 'pasted_text_pattern': self.pasted_text_pattern, 'key_value_delimiter': self.key_value_delimiter, 'ignore_pattern': self.ignore_pattern, 'field_to_parse': self.field_to_parse, 'field_name_to_id_match': self.field_name_to_id_match, 'field_name_to_id_replace': self.field_name_to_id_replace, 'key_value_end_pattern': self.key_value_end_pattern, 'field_not_found_for_updating_label': self.field_not_found_for_updating_label, 'invalid_field_values_not_in_list_label': self.invalid_field_values_not_in_list_label, 'debug_on': self.debug_on, 'xrefs': self._get_xref() } self.log.debug('[PasteParser] json.dumps(paste_parser_config)='+json.dumps(paste_parser_config)); add_script_data(req, {SCRIPT_VARIABLE_NAME: paste_parser_config}) return template, data, content_type # ITemplateProvider methods def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('PasteParser', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return []
class InterTracDispatcher(Component): """InterTrac dispatcher.""" implements(IRequestHandler, IWikiMacroProvider) intertrac_section = ConfigSection( 'intertrac', """This section configures InterTrac prefixes. Options in this section whose name contain a "." define aspects of the InterTrac prefix corresponding to the option name up to the ".". Options whose name don't contain a "." define an alias. The `.url` is mandatory and is used for locating the other Trac. This can be a relative URL in case that Trac environment is located on the same server. The `.title` information is used for providing a useful tooltip when moving the cursor over an InterTrac link. The `.compat` option can be used to activate or disable a ''compatibility'' mode: * If the targeted Trac is running a version below [trac:milestone:0.10 0.10] ([trac:r3526 r3526] to be precise), then it doesn't know how to dispatch an InterTrac link, and it's up to the local Trac to prepare the correct link. Not all links will work that way, but the most common do. This is called the compatibility mode, and is `true` by default. * If you know that the remote Trac knows how to dispatch InterTrac links, you can explicitly disable this compatibility mode and then ''any'' TracLinks can become InterTrac links. Example configuration: {{{ [intertrac] # -- Example of setting up an alias: t = trac # -- Link to an external Trac: trac.title = Edgewall's Trac for Trac trac.url = http://trac.edgewall.org }}} """) # IRequestHandler methods def match_request(self, req): match = re.match(r'^/intertrac/(.*)', req.path_info) if match: if match.group(1): req.args['link'] = match.group(1) return True def process_request(self, req): link = req.args.get('link', '') parts = link.split(':', 1) if len(parts) > 1: resolver, target = parts if target[:1] + target[-1:] not in ('""', "''"): link = '%s:"%s"' % (resolver, target) from trac.web.chrome import web_context link_frag = extract_link(self.env, web_context(req), link) if isinstance(link_frag, (Element, Fragment)): elt = find_element(link_frag, 'href') if elt is None: # most probably no permissions to view raise PermissionError(_("Can't view %(link)s:", link=link)) href = elt.attrib.get('href') else: href = req.href(link.rstrip(':')) req.redirect(href) # IWikiMacroProvider methods def get_macros(self): yield 'InterTrac' def get_macro_description(self, name): return 'messages', N_("Provide a list of known InterTrac prefixes.") def expand_macro(self, formatter, name, content): intertracs = {} for key, value in self.intertrac_section.options(): idx = key.rfind('.') if idx > 0: # 0 itself doesn't help much: .xxx = ... prefix, attribute = key[:idx], key[idx + 1:] intertrac = intertracs.setdefault(prefix, {}) intertrac[attribute] = value else: intertracs[key] = value # alias if 'trac' not in intertracs: intertracs['trac'] = { 'title': _('The Trac Project'), 'url': 'http://trac.edgewall.org' } def generate_prefix(prefix): intertrac = intertracs[prefix] if isinstance(intertrac, basestring): yield tag.tr(tag.td(tag.b(prefix)), tag.td('Alias for ', tag.b(intertrac))) else: url = intertrac.get('url', '') if url: title = intertrac.get('title', url) yield tag.tr( tag.td(tag.a(tag.b(prefix), href=url + '/timeline')), tag.td(tag.a(title, href=url))) return tag.table(class_="wiki intertrac")( tag.tr(tag.th(tag.em('Prefix')), tag.th(tag.em('Trac Site'))), [generate_prefix(p) for p in sorted(intertracs.keys())])
class KeywordLabelsModule(Component): implements(IRequestFilter, ITemplateProvider, ITemplateStreamFilter) ticketlink_query = Option('query', 'ticketlink_query', default='?status=!closed') keyword_labels_section = ConfigSection('keyword-labels', """In this section, you can define custom label colors.""") # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): path = req.path_info if path.startswith('/ticket/') or path.startswith('/newticket'): if data and 'ticket' in data: ticket = data['ticket'] keywords = ticket['keywords'] or '' for field in data.get('fields', ''): if field.get('name') == 'keywords': from trac.ticket.query import QueryModule if not (isinstance(keywords, basestring) and self.env.is_component_enabled(QueryModule)): break context = web_context(req, ticket) field['rendered'] = self._query_link_words(context, 'keywords', keywords, 'keyword-label ticket') return template, data, content_type # ITemplateProvider methods def get_htdocs_dirs(self): yield 'keyword_labels', resource_filename(__name__, 'htdocs') def get_templates_dirs(self): return [] # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if req.path_info.startswith('/report') or req.path_info.startswith('/query'): from trac.ticket.query import QueryModule from trac.ticket.model import Ticket if not (self.env.is_component_enabled(QueryModule)) \ and 'tickets' not in data \ and 'row_groups' not in data: return stream reported_tickets = [] if 'tickets' in data: class_ = 'keyword-label query' for row in data['tickets']: try: ticket = Ticket(self.env, row['id']) except KeyError: continue else: reported_tickets.insert(0, ticket) elif 'row_groups' in data: class_ = 'keyword-label report' for group in data['row_groups']: for row in group[1]: try: ticket = Ticket(self.env, row['resource'].id) except KeyError: continue else: reported_tickets.insert(0, ticket) def find_change(stream): ticket = reported_tickets.pop() keywords = ticket['keywords'] or '' context = web_context(req, ticket) tag_ = self._query_link_words(context, 'keywords', keywords, class_, prepend=[tag.span(' ')]) return itertools.chain(stream[0:5], tag_, stream[6:]) xpath = '//table[@class="listing tickets"]/tbody/tr/td[@class="summary"]' stream |= Transformer(xpath).filter(find_change) add_stylesheet(req, 'keyword_labels/css/keyword_labels.css') return stream # Inner methods def _query_link_words(self, context, name, value, class_, prepend=None, append=None): """Splits a list of words and makes a query link to each separately""" from trac.ticket.query import QueryModule if not (isinstance(value, basestring) and # None or other non-splitable self.env.is_component_enabled(QueryModule)): return value args = arg_list_to_args(parse_arg_list(self.ticketlink_query)) items = [] if prepend: items.extend(prepend) for i, word in enumerate(re.split(r'([;,\s]+)', value)): if i % 2: items.append(' ') elif word: backgroundColor = self.keyword_labels_section.get(word.lower()) fontColor = self.keyword_labels_section.get(word.lower() + '.font_color', 'white') if not backgroundColor: backgroundColor = ColorHash(word.encode('utf-8')).hex styles = { 'backgroundColor': backgroundColor, 'fontColor': fontColor, } word_args = args.copy() word_args[name] = '~' + word items.append(tag.a(word, style='background-color: {backgroundColor}; color: {fontColor}'.format(**styles), class_=class_, href=context.href.query(word_args))) if append: items.extend(append) return tag(items)
class TracIniAdminPanel(Component): """ An editor panel for trac.ini. """ implements(IAdminPanelProvider, ITemplateProvider) valid_section_name_chars = Option( 'ini-editor', 'valid-section-name-chars', '^[a-zA-Z0-9\\-_\\:]+$', doc="""Defines the valid characters for a section name or option name in `trac.ini`. Must be a valid regular expression. You only need to change these if you have plugins that use some strange section or option names. """, doc_domain="inieditorpanel") valid_option_name_chars = Option( 'ini-editor', 'valid-option-name-chars', '^[a-zA-Z0-9\\-_\\:.]+$', doc="""Defines the valid characters for a section name or option name in `trac.ini`. Must be a valid regular expression. You only need to change these if you have plugins that use some strange section or option names. """, doc_domain="inieditorpanel") security_manager = ExtensionOption( 'ini-editor', 'security-manager', IOptionSecurityManager, 'IniEditorEmptySecurityManager', doc="""Defines the security manager that specifies whether the user has access to certain options. """, doc_domain="inieditorpanel") # See "IniEditorBasicSecurityManager" for why we use a pipe char here. password_options = ListOption( 'ini-editor', 'password-options', doc="""Defines option fields (as `section-name|option-name`) that represent passwords. Password input fields are used for these fields. Note the fields specified here are taken additionally to some predefined fields provided by the ini editor. """, doc_domain="inieditorpanel") ini_section = ConfigSection( 'ini-editor', """This section is used to handle configurations used by TracIniAdminPanel plugin.""", doc_domain='inieditorpanel') DEFAULT_PASSWORD_OPTIONS = {'notification|smtp_password': True} def __init__(self): """Set up translation domain""" locale_dir = resource_filename(__name__, 'locale') add_domain(self.env.path, locale_dir) self.valid_section_name_chars_regexp = re.compile( self.valid_section_name_chars) self.valid_option_name_chars_regexp = re.compile( self.valid_option_name_chars) self.password_options_set = copy.deepcopy( self.DEFAULT_PASSWORD_OPTIONS) for option in self.password_options: self.password_options_set[option] = True # # IAdminPanelProvider methods # def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('general', dgettext('messages', 'General'), 'trac_ini_editor', _('trac.ini Editor')) def render_admin_panel(self, req, cat, page, path_info): req.perm.require('TRAC_ADMIN') if path_info == None: ext = "" else: ext = '/' + path_info # # Gather section names for section drop down field # all_section_names = [] for section_name in self.config.sections(): if section_name == 'components': continue all_section_names.append(section_name) # Check whether section exists and if it's not existing then check whether # its name is a valid section name. if (path_info is not None) and (path_info not in ('', '/', '_all_sections')) \ and (path_info not in all_section_names): if path_info == 'components': add_warning( req, _('The section "components" can\'t be edited with the ini editor.' )) req.redirect(req.href.admin(cat, page)) return None elif self.valid_section_name_chars_regexp.match(path_info) is None: add_warning(req, _('The section name %s is invalid.') % path_info) req.redirect(req.href.admin(cat, page)) return None # Add current section if it's not already in the list. This happens if # the section is essentially empty (i.e. newly created with no non-default # option values and no option from the option registry). all_section_names.append(path_info) registry = ConfigSection.get_registry(self.compmgr) descriptions = {} for section_name, section in registry.items(): if section_name == 'components': continue doc = section.__doc__ if not section_name in all_section_names: all_section_names.append(section_name) if doc: descriptions[section_name] = dgettext(section.doc_domain, doc) all_section_names.sort() sections = {} # # Check security manager # manager = None try: manager = self.security_manager except Exception, detail: # "except ... as ..." is only available since Python 2.6 if req.method != 'POST': # only add this warning once add_warning( req, _('Security manager could not be initated. %s') % unicode(detail)) if manager is None: # # Security manager is not available # if req.method == 'POST': req.redirect(req.href.admin(cat, page) + ext) return None elif req.method == 'POST' and 'change-section' in req.args: # # Changing the section # req.redirect( req.href.admin(cat, page) + '/' + req.args['change-section']) return None elif req.method == 'POST' and 'new-section-name' in req.args: # # Create new section (essentially simply changing the section) # section_name = req.args['new-section-name'].strip() if section_name == '': add_warning(req, _('The section name was empty.')) req.redirect(req.href.admin(cat, page) + ext) elif section_name == 'components': add_warning( req, _('The section "components" can\'t be edited with the ini editor.' )) req.redirect(req.href.admin(cat, page)) elif self.valid_section_name_chars_regexp.match( section_name) is None: add_warning( req, _('The section name %s is invalid.') % section_name) req.redirect(req.href.admin(cat, page) + ext) else: if section_name not in all_section_names: add_notice( req, _('Section %s has been created. Note that you need to add at least one option to store it permanently.' ) % section_name) else: add_warning(req, _('The section already exists.')) req.redirect(req.href.admin(cat, page) + '/' + section_name) return None elif path_info is not None and path_info not in ('', '/'): # # Display and possibly modify section (if one is selected) # default_values = self.config.defaults() # Gather option values # NOTE: This needs to be done regardless whether we have POST data just to # be on the safe site. if path_info == '_all_sections': # All sections custom_options = self._get_session_custom_options(req) # Only show sections with any data for section_name in all_section_names: sections[section_name] = self._read_section_config( req, section_name, default_values, custom_options) else: # Only a single section # Note: At this point path_info has already been verified to contain a # valid section name (see check above). sections[path_info] = self._read_section_config( req, path_info, default_values) # # Handle POST data # if req.method == 'POST': # Overwrite option values with POST values so that they don't get lost for key, value in req.args.items(): if not key.startswith( 'inieditor_value##'): # skip unrelated args continue name = key[len('inieditor_value##'):].split('##') section_name = name[0].strip() option_name = name[1].strip() if section_name == 'components': continue if option_name == 'dummy': if section_name not in sections: sections[section_name] = {} continue section = sections.get(section_name, None) if section: option = section.get(option_name, None) if option: self._set_option_value(req, section_name, option_name, option, value) else: # option not available; was propably newly added section[ option_name] = self._create_new_field_instance( req, section_name, option_name, default_values.get(section_name, None), value) else: # newly created section (not yet stored) sections[section_name] = { option_name: self._create_new_field_instance( req, section_name, option_name, None, value) } # Check which options use their default values # NOTE: Must be done after assigning field value from the previous step # to ensure that the default value has been initialized. if 'inieditor_default' in req.args: default_using_options = req.args.get('inieditor_default') if default_using_options is None or len( default_using_options) == 0: # if no checkbox was selected make this explicitly a list (just for safety) default_using_options = [] elif type(default_using_options).__name__ != 'list': # if there's only one checkbox it's just a string default_using_options = [ unicode(default_using_options) ] for default_using_option in default_using_options: name = default_using_option.split('##') section_name = name[0].strip() option_name = name[1].strip() section = sections.get(section_name, None) if section: option = section.get(option_name, None) if option: if option['access'] == ACCESS_MODIFIABLE: option['value'] = option['default_value'] else: # option not available; was propably newly added section[ option_name] = self._create_new_field_instance( req, section_name, option_name, default_values.get(section_name, None)) else: # newly created section (not yet stored) sections[section_name] = { option_name: self._create_new_field_instance( req, section_name, option_name, None) } # # Identify submit type # NOTE: Using "cur_focused_field" is a hack to support hitting the # return key even for the new-options field. Without this hitting # return would always associated to the apply button. # submit_type = None cur_focused_field = req.args.get('inieditor_cur_focused_field', '') if cur_focused_field.startswith('option-value-'): submit_type = 'apply-' + cur_focused_field[ len('option-value-'):] elif cur_focused_field.startswith('new-options-'): submit_type = 'addnewoptions-' + cur_focused_field[ len('new-options-'):] else: for key in req.args: if not key.startswith('inieditor-submit-'): continue submit_type = key[len('inieditor-submit-'):] break if submit_type.startswith('apply'): # apply changes if submit_type.startswith('apply-'): # apply only one section section_name = submit_type[len('apply-'):].strip() if self._apply_section_changes(req, section_name, sections[section_name]): add_notice( req, _('Changes for section %s have been applied.') % section_name) self.config.save() else: add_warning(req, _('No changes have been applied.')) else: # apply all sections changes_applied = False for section_name, options in sections.items(): if self._apply_section_changes( req, section_name, options): changes_applied = True if changes_applied: add_notice(req, _('Changes have been applied.')) self.config.save() else: add_warning(req, _('No changes have been applied.')) elif submit_type.startswith('discard'): if submit_type.startswith('discard-'): # discard only one section section_name = submit_type[len('discard-'):].strip() self._discard_section_changes(req, section_name, sections[section_name]) add_notice( req, _('Your changes for section %s have been discarded.' ) % section_name) else: # discard all sections for section_name, options in sections.items(): self._discard_section_changes( req, section_name, options) add_notice(req, _('All changes have been discarded.')) elif submit_type.startswith('addnewoptions-'): section_name = submit_type[len('addnewoptions-'):].strip() section = sections[section_name] new_option_names = req.args['new-options-' + section_name].split(',') section_default_values = default_values.get( section_name, None) field_added = False for new_option_name in new_option_names: new_option_name = new_option_name.strip() if new_option_name in section: continue # field already exists if self.valid_option_name_chars_regexp.match( new_option_name) is None: add_warning( req, _('The option name %s is invalid.') % new_option_name) continue new_option = self._create_new_field_instance( req, section_name, new_option_name, section_default_values) if new_option['access'] != ACCESS_MODIFIABLE: add_warning( req, _('The new option %s could not be added due to security restrictions.' ) % new_option_name) continue self._add_session_custom_option( req, section_name, new_option_name) field_added = True if field_added: add_notice( req, _('The new fields have been added to section %s.') % section_name) else: add_warning(req, _('No new fields have been added.')) req.redirect(req.href.admin(cat, page) + ext) return None # Split sections dict for faster template rendering modifiable_options = {} readonly_options = {} hidden_options = {} for section_name, options in sections.items(): sect_modifiable = {} sect_readonly = {} sect_hidden = {} for option_name, option in options.items(): if option['access'] == ACCESS_MODIFIABLE: sect_modifiable[option_name] = option elif option['access'] == ACCESS_READONLY: sect_readonly[option_name] = option else: sect_hidden[option_name] = option modifiable_options[section_name] = sect_modifiable readonly_options[section_name] = sect_readonly hidden_options[section_name] = sect_hidden registry = ConfigSection.get_registry(self.compmgr) descriptions = {} for name, section in registry.items(): doc = section.__doc__ if doc: descriptions[name] = dgettext(section.doc_domain, doc) data = { 'all_section_names': all_section_names, 'sections': sections, 'descriptions': descriptions, 'modifiable_options': modifiable_options, 'readonly_options': readonly_options, 'hidden_options': hidden_options } section_counters = {} settings_stored_values = {} for section_name, section in sections.iteritems(): escaped = section_name.replace(':', '_') section_counters[escaped] = {'option_count': len(section)} settings_stored_values[escaped] = dict( (name, option['stored_value']) for name, option in modifiable_options[section_name].iteritems() if option['type'] != 'password') add_script_data( req, { 'section_count': len(sections), 'section_names': sorted(section_counters), 'section_counters': section_counters, 'settings_stored_values': settings_stored_values, 'info_format': _("Modified: %(mod)d | Defaults: %(def)d | Options " "count: %(opt)d"), }) add_stylesheet(req, 'inieditorpanel/main.css') add_script(req, 'inieditorpanel/editor.js') return 'admin_tracini.html', data
class NotificationSystem(Component): email_sender = ExtensionOption( 'notification', 'email_sender', IEmailSender, 'SmtpEmailSender', """Name of the component implementing `IEmailSender`. This component is used by the notification system to send emails. Trac currently provides `SmtpEmailSender` for connecting to an SMTP server, and `SendmailEmailSender` for running a `sendmail`-compatible executable. (''since 0.12'')""") smtp_enabled = BoolOption('notification', 'smtp_enabled', 'false', """Enable email notification.""") smtp_from = Option( 'notification', 'smtp_from', 'trac@localhost', """Sender address to use in notification emails. At least one of `smtp_from` and `smtp_replyto` must be set, otherwise Trac refuses to send notification mails.""") smtp_from_name = Option('notification', 'smtp_from_name', '', """Sender name to use in notification emails.""") smtp_from_author = BoolOption( 'notification', 'smtp_from_author', 'false', """Use the author of the change as the sender in notification emails (e.g. reporter of a new ticket, author of a comment). If the author hasn't set an email address, `smtp_from` and `smtp_from_name` are used instead. (''since 1.0'')""") smtp_replyto = Option( 'notification', 'smtp_replyto', 'trac@localhost', """Reply-To address to use in notification emails. At least one of `smtp_from` and `smtp_replyto` must be set, otherwise Trac refuses to send notification mails.""") smtp_always_cc_list = ListOption( 'notification', 'smtp_always_cc', '', sep=(',', ' '), doc="""Comma-separated list of email addresses to always send notifications to. Addresses can be seen by all recipients (Cc:).""") smtp_always_bcc_list = ListOption( 'notification', 'smtp_always_bcc', '', sep=(',', ' '), doc="""Comma-separated list of email addresses to always send notifications to. Addresses are not public (Bcc:). """) smtp_default_domain = Option( 'notification', 'smtp_default_domain', '', """Default host/domain to append to addresses that do not specify one. Fully qualified addresses are not modified. The default domain is appended to all username/login for which an email address cannot be found in the user settings.""") ignore_domains_list = ListOption( 'notification', 'ignore_domains', '', doc="""Comma-separated list of domains that should not be considered part of email addresses (for usernames with Kerberos domains).""") admit_domains_list = ListOption( 'notification', 'admit_domains', '', doc="""Comma-separated list of domains that should be considered as valid for email addresses (such as localdomain).""") mime_encoding = Option( 'notification', 'mime_encoding', 'none', """Specifies the MIME encoding scheme for emails. Supported values are: `none`, the default value which uses 7-bit encoding if the text is plain ASCII or 8-bit otherwise. `base64`, which works with any kind of content but may cause some issues with touchy anti-spam/anti-virus engine. `qp` or `quoted-printable`, which works best for european languages (more compact than base64) if 8-bit encoding cannot be used. """) use_public_cc = BoolOption( 'notification', 'use_public_cc', 'false', """Addresses in the To and Cc fields are visible to all recipients. If this option is disabled, recipients are put in the Bcc list. """) use_short_addr = BoolOption( 'notification', 'use_short_addr', 'false', """Permit email address without a host/domain (i.e. username only). The SMTP server should accept those addresses, and either append a FQDN or use local delivery. See also `smtp_default_domain`. Do not use this option with a public SMTP server. """) smtp_subject_prefix = Option( 'notification', 'smtp_subject_prefix', '__default__', """Text to prepend to subject line of notification emails. If the setting is not defined, then `[$project_name]` is used as the prefix. If no prefix is desired, then specifying an empty option will disable it. """) notification_subscriber_section = ConfigSection( 'notification-subscriber', """The notifications subscriptions are controlled by plugins. All `INotificationSubscriber` components are in charge. These components may allow to be configured via this section in the `trac.ini` file. See TracNotification for more details. Available subscribers: [[SubscriberList]] """) distributors = ExtensionPoint(INotificationDistributor) subscribers = ExtensionPoint(INotificationSubscriber) @property def smtp_always_cc(self): # For backward compatibility return self.config.get('notification', 'smtp_always_cc') @property def smtp_always_bcc(self): # For backward compatibility return self.config.get('notification', 'smtp_always_bcc') @property def ignore_domains(self): # For backward compatibility return self.config.get('notification', 'ignore_domains') @property def admit_domains(self): # For backward compatibility return self.config.get('notification', 'admit_domains') @lazy def subscriber_defaults(self): rawsubscriptions = self.notification_subscriber_section.options() return parse_subscriber_config(rawsubscriptions) def default_subscriptions(self, klass): for d in self.subscriber_defaults[klass]: yield (klass, d['distributor'], d['format'], d['priority'], d['adverb']) def send_email(self, from_addr, recipients, message): """Send message to recipients via e-mail.""" self.email_sender.send(from_addr, recipients, message) def notify(self, event): """Distribute an event to all subscriptions. :param event: a `NotificationEvent` """ self.distribute_event(event, self.subscriptions(event)) def distribute_event(self, event, subscriptions): """Distribute a event to all subscriptions. :param event: a `NotificationEvent` :param subscriptions: a list of tuples (sid, authenticated, address, transport, format) where either sid or address can be `None` """ packages = {} for sid, authenticated, address, transport, format in subscriptions: package = packages.setdefault(transport, set()) package.add((sid, authenticated, address, format)) for distributor in self.distributors: for transport in distributor.transports(): if transport in packages: recipients = list(packages[transport]) distributor.distribute(transport, recipients, event) def subscriptions(self, event): """Return all subscriptions for a given event. :return: a list of (sid, authenticated, address, transport, format) """ subscriptions = [] for subscriber in self.subscribers: subscriptions.extend(x for x in subscriber.matches(event) if x) # For each (transport, sid, authenticated) combination check the # subscription with the highest priority: # If it is "always" keep it. If it is "never" drop it. # sort by (transport, sid, authenticated, priority) ordered = sorted(subscriptions, key=itemgetter(1, 2, 3, 6)) previous_combination = None for rule, transport, sid, auth, addr, fmt, prio, adverb in ordered: if (transport, sid, auth) == previous_combination: continue if adverb == 'always': self.log.debug( "Adding (%s [%s]) for 'always' on rule (%s) " "for (%s)", sid, auth, rule, transport) yield (sid, auth, addr, transport, fmt) else: self.log.debug( "Ignoring (%s [%s]) for 'never' on rule (%s) " "for (%s)", sid, auth, rule, transport) # Also keep subscriptions without sid (raw email subscription) if sid: previous_combination = (transport, sid, auth)
def render_admin_panel(self, req, cat, page, path_info): req.perm.require('TRAC_ADMIN') if path_info == None: ext = "" else: ext = '/' + path_info # # Gather section names for section drop down field # all_section_names = [] for section_name in self.config.sections(): if section_name == 'components': continue all_section_names.append(section_name) # Check whether section exists and if it's not existing then check whether # its name is a valid section name. if (path_info is not None) and (path_info not in ('', '/', '_all_sections')) \ and (path_info not in all_section_names): if path_info == 'components': add_warning( req, _('The section "components" can\'t be edited with the ini editor.' )) req.redirect(req.href.admin(cat, page)) return None elif self.valid_section_name_chars_regexp.match(path_info) is None: add_warning(req, _('The section name %s is invalid.') % path_info) req.redirect(req.href.admin(cat, page)) return None # Add current section if it's not already in the list. This happens if # the section is essentially empty (i.e. newly created with no non-default # option values and no option from the option registry). all_section_names.append(path_info) registry = ConfigSection.get_registry(self.compmgr) descriptions = {} for section_name, section in registry.items(): if section_name == 'components': continue doc = section.__doc__ if not section_name in all_section_names: all_section_names.append(section_name) if doc: descriptions[section_name] = dgettext(section.doc_domain, doc) all_section_names.sort() sections = {} # # Check security manager # manager = None try: manager = self.security_manager except Exception, detail: # "except ... as ..." is only available since Python 2.6 if req.method != 'POST': # only add this warning once add_warning( req, _('Security manager could not be initated. %s') % unicode(detail))
class ConfigurableTicketWorkflow(Component): """Ticket action controller which provides actions according to a workflow defined in trac.ini. The workflow is defined in the `[ticket-workflow]` section of the [wiki:TracIni#ticket-workflow-section trac.ini] configuration file. """ implements(IEnvironmentSetupParticipant, ITicketActionController) ticket_workflow_section = ConfigSection('ticket-workflow', """The workflow for tickets is controlled by plugins. By default, there's only a `ConfigurableTicketWorkflow` component in charge. That component allows the workflow to be configured via this section in the `trac.ini` file. See TracWorkflow for more details. """) operations = ('del_owner', 'set_owner', 'set_owner_to_self', 'may_set_owner', 'set_resolution', 'del_resolution', 'leave_status', 'reset_workflow') def __init__(self): self.actions = self.get_all_actions() self.log.debug('Workflow actions at initialization: %s\n', self.actions) # IEnvironmentSetupParticipant methods def environment_created(self): """When an environment is created, we provide the basic-workflow, unless a ticket-workflow section already exists. """ if 'ticket-workflow' not in self.config.sections(): load_workflow_config_snippet(self.config, 'basic-workflow.ini') self.config.save() self.actions = self.get_all_actions() def environment_needs_upgrade(self): pass def upgrade_environment(self): pass # ITicketActionController methods def get_ticket_actions(self, req, ticket): """Returns a list of (weight, action) tuples that are valid for this request and this ticket.""" # Get the list of actions that can be performed # Determine the current status of this ticket. If this ticket is in # the process of being modified, we need to base our information on the # pre-modified state so that we don't try to do two (or more!) steps at # once and get really confused. ticket_status = ticket._old.get('status', ticket['status']) exists = ticket_status is not None ticket_owner = ticket._old.get('owner', ticket['owner']) author = get_reporter_id(req, 'author') resource = ticket.resource allowed_actions = [] for action_name, action_info in self.actions.items(): operations = action_info['operations'] newstate = action_info['newstate'] # Exclude action that is effectively a No-op. if len(operations) == 1 and \ operations[0] == 'set_owner_to_self' and \ ticket_owner == author and ticket_status == newstate: continue if operations and not \ any(opt in self.operations for opt in operations): continue # Ignore operations not defined by this controller oldstates = action_info['oldstates'] if exists and oldstates == ['*'] or ticket_status in oldstates: # This action is valid in this state. Check permissions. if self._is_action_allowed(req, action_info, resource): allowed_actions.append((action_info['default'], action_name)) # Append special `_reset` action if status is invalid. if exists and '_reset' in self.actions and \ ticket_status not in TicketSystem(self.env).get_all_status(): reset = self.actions['_reset'] if self._is_action_allowed(req, reset, resource): allowed_actions.append((reset['default'], '_reset')) return allowed_actions def _is_action_allowed(self, req, action, resource): """Returns `True` if the workflow action is allowed for the `resource`. """ perm_cache = req.perm(resource) required_perms = action['permissions'] if required_perms: for permission in required_perms: if permission in perm_cache: break else: return False return True def get_all_status(self): """Return a list of all states described by the configuration. """ all_status = set() for attributes in self.actions.values(): all_status.update(attributes['oldstates']) all_status.add(attributes['newstate']) all_status.discard('*') all_status.discard('') all_status.discard(None) return all_status def render_ticket_action_control(self, req, ticket, action): self.log.debug('render_ticket_action_control: action "%s"', action) this_action = self.actions[action] status = this_action['newstate'] operations = this_action['operations'] ticket_owner = ticket._old.get('owner', ticket['owner']) ticket_status = ticket._old.get('status', ticket['status']) author = get_reporter_id(req, 'author') author_info = partial(Chrome(self.env).authorinfo, req, resource=ticket.resource) format_author = partial(Chrome(self.env).format_author, req, resource=ticket.resource) formatted_current_owner = author_info(ticket_owner) exists = ticket_status is not None ticket_system = TicketSystem(self.env) control = [] # default to nothing hints = [] if 'reset_workflow' in operations: control.append(_("from invalid state")) hints.append(_("Current state no longer exists")) if 'del_owner' in operations: hints.append(_("The ticket will be disowned")) if 'set_owner' in operations or 'may_set_owner' in operations: owners = self.get_allowed_owners(req, ticket, this_action) if 'set_owner' in operations: default_owner = author elif 'may_set_owner' in operations: if not exists: default_owner = ticket_system.default_owner else: default_owner = ticket_owner or None if owners is not None and default_owner not in owners: owners.insert(0, default_owner) else: # Protect against future modification for case that another # operation is added to the outer conditional raise AssertionError(operations) id = 'action_%s_reassign_owner' % action if not owners: owner = req.args.get(id, default_owner) control.append( tag_("to %(owner)s", owner=tag.input(type='text', id=id, name=id, value=owner))) if not exists or ticket_owner is None: hints.append(_("The owner will be the specified user")) else: hints.append(tag_("The owner will be changed from " "%(current_owner)s to the specified " "user", current_owner=formatted_current_owner)) elif len(owners) == 1: owner = tag.input(type='hidden', id=id, name=id, value=owners[0]) formatted_new_owner = author_info(owners[0]) control.append(tag_("to %(owner)s", owner=tag(formatted_new_owner, owner))) if not exists or ticket_owner is None: hints.append(tag_("The owner will be %(new_owner)s", new_owner=formatted_new_owner)) elif ticket['owner'] != owners[0]: hints.append(tag_("The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_new_owner)) else: selected_owner = req.args.get(id, default_owner) control.append(tag_("to %(owner)s", owner=tag.select( [tag.option(label, value=value if value is not None else '', selected=(value == selected_owner or None)) for label, value in sorted((format_author(owner), owner) for owner in owners)], id=id, name=id))) if not exists or ticket_owner is None: hints.append(_("The owner will be the selected user")) else: hints.append(tag_("The owner will be changed from " "%(current_owner)s to the selected user", current_owner=formatted_current_owner)) elif 'set_owner_to_self' in operations: formatted_author = author_info(author) if not exists or ticket_owner is None: hints.append(tag_("The owner will be %(new_owner)s", new_owner=formatted_author)) elif ticket_owner != author: hints.append(tag_("The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_author)) elif ticket_status != status: hints.append(tag_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner)) if 'set_resolution' in operations: resolutions = [r.name for r in Resolution.select(self.env)] if 'set_resolution' in this_action: valid_resolutions = set(resolutions) resolutions = this_action['set_resolution'] if any(x not in valid_resolutions for x in resolutions): raise ConfigurationError(_( "Your workflow attempts to set a resolution but uses " "undefined resolutions (configuration issue, please " "contact your Trac admin).")) if not resolutions: raise ConfigurationError(_( "Your workflow attempts to set a resolution but none is " "defined (configuration issue, please contact your Trac " "admin).")) id = 'action_%s_resolve_resolution' % action if len(resolutions) == 1: resolution = tag.input(type='hidden', id=id, name=id, value=resolutions[0]) control.append(tag_("as %(resolution)s", resolution=tag(resolutions[0], resolution))) hints.append(tag_("The resolution will be set to %(name)s", name=resolutions[0])) else: selected_option = req.args.get(id, ticket_system.default_resolution) control.append(tag_("as %(resolution)s", resolution=tag.select( [tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions], id=id, name=id))) hints.append(_("The resolution will be set")) if 'del_resolution' in operations: hints.append(_("The resolution will be deleted")) if 'leave_status' in operations: control.append(tag_("as %(status)s", status=ticket_status)) if len(operations) == 1: hints.append(tag_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner) if ticket_owner else _("The ticket will remain with no owner")) elif not operations: if status != '*': if ticket['status'] is None: hints.append(tag_("The status will be '%(name)s'", name=status)) else: hints.append(tag_("Next status will be '%(name)s'", name=status)) return (this_action['label'], tag(separated(control, ' ')), tag(separated(hints, '. ', '.') if hints else '')) def get_ticket_changes(self, req, ticket, action): this_action = self.actions[action] # Enforce permissions if not self._is_action_allowed(req, this_action, ticket.resource): # The user does not have any of the listed permissions, so we won't # do anything. return {} updated = {} # Status changes status = this_action['newstate'] if status != '*': updated['status'] = status for operation in this_action['operations']: if operation == 'del_owner': updated['owner'] = '' elif operation in ('set_owner', 'may_set_owner'): set_owner = this_action.get('set_owner') newowner = req.args.get('action_%s_reassign_owner' % action, set_owner[0] if set_owner else '') # If there was already an owner, we get a list, [new, old], # but if there wasn't we just get new. if type(newowner) == list: newowner = newowner[0] updated['owner'] = self._sub_owner_keyword(newowner, ticket) elif operation == 'set_owner_to_self': updated['owner'] = get_reporter_id(req, 'author') elif operation == 'del_resolution': updated['resolution'] = '' elif operation == 'set_resolution': set_resolution = this_action.get('set_resolution') newresolution = req.args.get('action_%s_resolve_resolution' % action, set_resolution[0] if set_resolution else '') updated['resolution'] = newresolution # reset_workflow is just a no-op here, so we don't look for it. # leave_status is just a no-op here, so we don't look for it. # Set owner to component owner for 'new' ticket if: # - ticket doesn't exist and owner is < default > # - component is changed # - owner isn't explicitly changed # - ticket owner is equal to owner of previous component # - new component has an owner if not ticket.exists and 'owner' not in updated: updated['owner'] = self._sub_owner_keyword(ticket['owner'], ticket) elif ticket['status'] == 'new' and \ 'component' in ticket.values and \ 'component' in ticket._old and \ 'owner' not in updated: try: old_comp = TicketComponent(self.env, ticket._old['component']) except ResourceNotFound: # If the old component has been removed from the database # we just leave the owner as is. pass else: old_owner = old_comp.owner or '' current_owner = ticket['owner'] or '' if old_owner == current_owner: new_comp = TicketComponent(self.env, ticket['component']) if new_comp.owner: updated['owner'] = new_comp.owner return updated def apply_action_side_effects(self, req, ticket, action): pass # Public methods (for other ITicketActionControllers that want to use # our config file and provide an operation for an action) def get_all_actions(self): actions = parse_workflow_config(self.ticket_workflow_section.options()) has_new_state = any('new' in [a['newstate']] + a['oldstates'] for a in actions.itervalues()) if has_new_state: # Special action that gets enabled if the current status no # longer exists, as no other action can then change its state. # (#5307/#11850) reset = { 'default': 0, 'label': 'Reset', 'newstate': 'new', 'oldstates': [], 'operations': ['reset_workflow'], 'permissions': ['TICKET_ADMIN'] } for key, val in reset.items(): actions['_reset'].setdefault(key, val) for name, info in actions.iteritems(): for val in ('<none>', '< none >'): sub_val(actions[name]['oldstates'], val, None) if not info['newstate']: self.log.warning("Ticket workflow action '%s' doesn't define " "any transitions", name) return actions def get_actions_by_operation(self, operation): """Return a list of all actions with a given operation (for use in the controller's get_all_status()) """ actions = [(info['default'], action) for action, info in self.actions.items() if operation in info['operations']] return actions def get_actions_by_operation_for_req(self, req, ticket, operation): """Return list of all actions with a given operation that are valid in the given state for the controller's get_ticket_actions(). If state='*' (the default), all actions with the given operation are returned. """ # Be sure to look at the original status. status = ticket._old.get('status', ticket['status']) actions = [(info['default'], action) for action, info in self.actions.items() if operation in info['operations'] and ('*' in info['oldstates'] or status in info['oldstates']) and self._is_action_allowed(req, info, ticket.resource)] return actions # Public methods def get_allowed_owners(self, req, ticket, action): """Returns users listed in the `set_owner` field of the action or possessing the `TICKET_MODIFY` permission if `set_owner` is not specified. This method can be overridden in a subclasses in order to customize the list of users that populate the assign-to select box. :since: 1.3.2 """ if 'set_owner' in action: return self._to_users(action['set_owner'], ticket) elif TicketSystem(self.env).restrict_owner: users = PermissionSystem(self.env)\ .get_users_with_permission('TICKET_MODIFY') cache = partial(PermissionCache, self.env, resource=ticket.resource) return sorted(u for u in users if 'TICKET_MODIFY' in cache(username=u)) # Internal methods def _sub_owner_keyword(self, owner, ticket): """Substitute keywords from the default_owner field. < default > -> component owner """ if owner in ('< default >', '<default>'): default_owner = '' if ticket['component']: try: component = TicketComponent(self.env, ticket['component']) except ResourceNotFound: pass # No such component exists else: default_owner = component.owner # May be empty return default_owner return owner def _to_users(self, users_perms_and_groups, ticket): """Finds all users contained in the list of `users_perms_and_groups` by recursive lookup of users when a `group` is encountered. """ ps = PermissionSystem(self.env) groups = ps.get_groups_dict() def append_owners(users_perms_and_groups): for user_perm_or_group in users_perms_and_groups: if user_perm_or_group == 'authenticated': owners.update({u[0] for u in self.env.get_known_users()}) elif user_perm_or_group.isupper(): perm = user_perm_or_group for user in ps.get_users_with_permission(perm): if perm in PermissionCache(self.env, user, ticket.resource): owners.add(user) elif user_perm_or_group not in groups: owners.add(user_perm_or_group) else: append_owners(groups[user_perm_or_group]) owners = set() append_owners(users_perms_and_groups) return sorted(owners)
class PygmentsRenderer(Component): """HTML renderer for syntax highlighting based on Pygments.""" implements(ISystemInfoProvider, IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler, ITemplateProvider) is_valid_default_handler = False pygments_lexer_options = ConfigSection( 'pygments-lexer', """Configure Pygments [%(url)s lexer] options. For example, to set the [%(url)s#lexers-for-php-and-related-languages PhpLexer] options `startinline` and `funcnamehighlighting`: {{{#!ini [pygments-lexer] php.startinline = True php.funcnamehighlighting = True }}} The lexer name is derived from the class name, with `Lexer` stripped from the end. The lexer //short names// can also be used in place of the lexer name. """, doc_args={'url': 'http://pygments.org/docs/lexers/'}) default_style = Option( 'mimeviewer', 'pygments_default_style', 'trac', """The default style to use for Pygments syntax highlighting.""") pygments_modes = ListOption( 'mimeviewer', 'pygments_modes', '', doc="""List of additional MIME types known by Pygments. For each, a tuple `mimetype:mode:quality` has to be specified, where `mimetype` is the MIME type, `mode` is the corresponding Pygments mode to be used for the conversion and `quality` is the quality ratio associated to this conversion. That can also be used to override the default quality ratio used by the Pygments render.""") expand_tabs = True returns_source = True QUALITY_RATIO = 7 EXAMPLE = """<!DOCTYPE html> <html lang="en"> <head> <title>Hello, world!</title> <script> jQuery(function($) { $("h1").fadeIn("slow"); }); </script> </head> <body> <h1>Hello, world!</h1> </body> </html>""" # ISystemInfoProvider methods def get_system_info(self): yield 'Pygments', get_pkginfo(pygments).get('version') # IHTMLPreviewRenderer methods def get_extra_mimetypes(self): for _, aliases, _, mimetypes in get_all_lexers(): for mimetype in mimetypes: yield mimetype, aliases def get_quality_ratio(self, mimetype): # Extend default MIME type to mode mappings with configured ones try: return self._types[mimetype][1] except KeyError: return 0 def render(self, context, mimetype, content, filename=None, rev=None): req = context.req style = req.session.get('pygments_style', self.default_style) add_stylesheet(req, '/pygments/%s.css' % style) try: if len(content) > 0: mimetype = mimetype.split(';', 1)[0] language = self._types[mimetype][0] return self._generate(language, content, context) except (KeyError, ValueError): raise Exception("No Pygments lexer found for mime-type '%s'." % mimetype) # IPreferencePanelProvider methods def get_preference_panels(self, req): yield 'pygments', _('Syntax Highlighting') def render_preference_panel(self, req, panel): styles = list(get_all_styles()) if req.method == 'POST': style = req.args.get('style') if style and style in styles: req.session['pygments_style'] = style add_notice(req, _("Your preferences have been saved.")) req.redirect(req.href.prefs(panel or None)) for style in sorted(styles): add_stylesheet(req, '/pygments/%s.css' % style, title=style.title()) output = self._generate('html', self.EXAMPLE) return 'prefs_pygments.html', { 'output': output, 'selection': req.session.get('pygments_style', self.default_style), 'styles': styles } # IRequestHandler methods def match_request(self, req): match = re.match(r'/pygments/([-\w]+)\.css', req.path_info) if match: req.args['style'] = match.group(1) return True def process_request(self, req): style = req.args['style'] try: style_cls = get_style_by_name(style) except ValueError as e: raise HTTPNotFound(e) parts = style_cls.__module__.split('.') filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py') mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz) last_modified = http_date(mtime) if last_modified == req.get_header('If-Modified-Since'): req.send_response(304) req.end_headers() return formatter = HtmlFormatter(style=style_cls) content = u'\n\n'.join([ formatter.get_style_defs('div.code pre'), formatter.get_style_defs('table.code td') ]).encode('utf-8') req.send_response(200) req.send_header('Content-Type', 'text/css; charset=utf-8') req.send_header('Last-Modified', last_modified) req.send_header('Content-Length', len(content)) req.write(content) # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): return [resource_filename('trac.mimeview', 'templates')] # Internal methods @lazy def _lexer_alias_name_map(self): lexer_alias_name_map = {} for lexer_name, aliases, _, _ in get_all_lexers(): name = aliases[0] if aliases else lexer_name for alias in aliases: lexer_alias_name_map[alias] = name return lexer_alias_name_map @lazy def _lexer_options(self): lexer_options = {} for key, lexer_option_value in self.pygments_lexer_options.options(): try: lexer_name_or_alias, lexer_option_name = key.split('.') except ValueError: pass else: lexer_name = self._lexer_alias_to_name(lexer_name_or_alias) lexer_option = {lexer_option_name: lexer_option_value} lexer_options.setdefault(lexer_name, {}).update(lexer_option) return lexer_options @lazy def _types(self): types = {} for lexer_name, aliases, _, mimetypes in get_all_lexers(): name = aliases[0] if aliases else lexer_name for mimetype in mimetypes: types[mimetype] = (name, self.QUALITY_RATIO) # Pygments < 1.4 doesn't know application/javascript if 'application/javascript' not in types: js_entry = types.get('text/javascript') if js_entry: types['application/javascript'] = js_entry types.update(Mimeview(self.env).configured_modes_mapping('pygments')) return types def _generate(self, language, content, context=None): lexer_name = self._lexer_alias_to_name(language) lexer_options = {'stripnl': False} lexer_options.update(self._lexer_options.get(lexer_name, {})) if context: lexer_options.update(context.get_hint('lexer_options', {})) lexer = get_lexer_by_name(lexer_name, **lexer_options) out = io.StringIO() # Specify `lineseparator` to workaround exception with Pygments 2.2.0: # "TypeError: unicode argument expected, got 'str'" with newline input formatter = HtmlFormatter(nowrap=True, lineseparator=u'\n') formatter.format(lexer.get_tokens(content), out) return Markup(out.getvalue()) def _lexer_alias_to_name(self, alias): return self._lexer_alias_name_map.get(alias, alias)
class ProductEnvironment(Component, ComponentManager): """Bloodhound product-aware environment manager. Bloodhound encapsulates access to product resources stored inside a Trac environment via product environments. They are compatible lightweight irepresentations of top level environment. Product environments contain among other things: * configuration key-value pairs stored in the database, * product-aware clones of the wiki and ticket attachments files, Product environments do not have: * product-specific templates and plugins, * a separate database * active participation in database upgrades and other setup tasks See https://issues.apache.org/bloodhound/wiki/Proposals/BEP-0003 """ class __metaclass__(ComponentMeta): def product_env_keymap(args, kwds, kwd_mark): # Remove meta-reference to self (i.e. product env class) args = args[1:] try: product = kwds['product'] except KeyError: # Product provided as positional argument if isinstance(args[1], Product): args = (args[0], args[1].prefix) + args[2:] else: # Product supplied as keyword argument if isinstance(product, Product): kwds['product'] = product.prefix return default_keymap(args, kwds, kwd_mark) @lru_cache(maxsize=100, keymap=product_env_keymap) def __call__(self, *args, **kwargs): """Return an existing instance of there is a hit in the global LRU cache, otherwise create a new instance. """ return ComponentMeta.__call__(self, *args, **kwargs) del product_env_keymap implements(trac.env.ISystemInfoProvider, IPermissionRequestor) setup_participants = ExtensionPoint(trac.env.IEnvironmentSetupParticipant) multi_product_support_components = ExtensionPoint( ISupportMultiProductEnvironment) @property def product_setup_participants(self): return [ component for component in self.setup_participants if component not in self.multi_product_support_components ] components_section = ConfigSection( 'components', """This section is used to enable or disable components provided by plugins, as well as by Trac itself. See also: TracIni , TracPlugins """) @property def shared_plugins_dir(): """Product environments may not add plugins. """ return '' _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.""") @property def base_url(self): base_url = self._base_url if base_url == self.parent.base_url: return '' return base_url _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)''""") @property def project_name(self): """Name of the product. """ return self.product.name @property def project_description(self): """Short description of the product. """ return self.product.description @property def project_url(self): """URL of the main project web site, usually the website in which the `base_url` resides. This is used in notification e-mails. """ # FIXME: Should products have different values i.e. config option ? return self.parent.project_url project_admin = Option( 'project', 'admin', '', """E-Mail address of the product's leader / administrator.""") @property def project_footer(self): """Page footer text (right-aligned). """ # FIXME: Should products have different values i.e. config option ? return self.parent.project_footer project_icon = Option('project', 'icon', 'common/trac.ico', """URL of the icon of the product.""") log_type = Option( 'logging', 'log_type', 'inherit', """Logging facility to use. Should be one of (`inherit`, `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, env, product, create=False): """Initialize the product environment. :param env: the global Trac environment :param product: product prefix or an instance of multiproduct.model.Product """ if not isinstance(env, trac.env.Environment): cls = self.__class__ raise TypeError("Initializer must be called with " \ "trac.env.Environment instance as first argument " \ "(got %s instance instead)" % (cls.__module__ + '.' + cls.__name__, )) ComponentManager.__init__(self) if isinstance(product, Product): if product._env is not env: raise ValueError("Product's environment mismatch") elif isinstance(product, basestring): products = Product.select(env, where={'prefix': product}) if len(products) == 1: product = products[0] else: env.log.debug("Products for '%s' : %s", product, products) raise LookupError("Missing product %s" % (product, )) self.parent = env self.product = product self.systeminfo = [] self.setup_config() # when creating product environment, invoke `IEnvironmentSetupParticipant.environment_created` # for all setup participants that don't support multi product environments if create: for participant in self.product_setup_participants: with ComponentEnvironmentContext(self, participant): participant.environment_created() def __getitem__(self, cls): if issubclass(cls, trac.env.Environment): return self.parent elif cls is self.__class__: return self else: return ComponentManager.__getitem__(self, cls) def __getattr__(self, attrnm): """Forward attribute access request to parent environment. Initially this will affect the following members of `trac.env.Environment` class: system_info_providers, secure_cookies, project_admin_trac_url, get_system_info, get_version, get_templates_dir, get_templates_dir, get_log_dir, backup """ try: if attrnm in ('parent', '_rules'): raise AttributeError return getattr(self.parent, attrnm) except AttributeError: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attrnm)) def __repr__(self): return "<%s %s at %s>" % (self.__class__.__name__, repr(self.product.prefix), hex(id(self))) @lazy def path(self): """The subfolder `./products/<product prefix>` relative to the top-level directory of the global environment will be the root of product file system area. """ folder = os.path.join(self.parent.path, 'products', self.product.prefix) if not os.path.exists(folder): os.makedirs(folder) return folder # IPermissionRequestor methods def get_permission_actions(self): """Implement the product-specific `PRODUCT_ADMIN` meta permission. """ actions = set() permsys = PermissionSystem(self) for requestor in permsys.requestors: if requestor is not self and requestor is not permsys: for action in requestor.get_permission_actions() or []: if isinstance(action, tuple): actions.add(action[0]) else: actions.add(action) # PermissionSystem's method was not invoked actions.add('EMAIL_VIEW') # FIXME: should not be needed, JIC better double check actions.discard('TRAC_ADMIN') return [('PRODUCT_ADMIN', list(actions))] # ISystemInfoProvider methods # Same as parent environment's . Avoid duplicated code component_activated = trac.env.Environment.component_activated.im_func _component_name = trac.env.Environment._component_name.im_func _component_rules = trac.env.Environment._component_rules enable_component = trac.env.Environment.enable_component.im_func get_known_users = trac.env.Environment.get_known_users.im_func get_repository = trac.env.Environment.get_repository.im_func is_component_enabled_local = trac.env.Environment.is_component_enabled.im_func def is_component_enabled(self, cls): """Implemented to only allow activation of components already activated in the global environment that are in turn 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. """ if cls is self.__class__: # Prevent lookups in parent env ... will always fail return True # FIXME : Maybe checking for ComponentManager is too drastic elif issubclass(cls, ComponentManager): # Avoid clashes with overridden Environment's options return False elif self.parent[cls] is None: return False return self.is_component_enabled_local(cls) def get_db_cnx(self): """Return a database connection from the connection pool :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead `db_transaction` for obtaining the `db` database connection which can be used for performing any query (SELECT/INSERT/UPDATE/DELETE):: with env.db_transaction as db: ... `db_query` for obtaining a `db` database connection which can be used for performing SELECT queries only:: with env.db_query as db: ... """ return BloodhoundConnectionWrapper(self.parent.get_db_cnx(), self) @property 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, e: ... """ # exception types same as in global environment return self.parent.db_exc def with_transaction(self, db=None): """Decorator for transaction functions :deprecated: """ raise NotImplementedError('Deprecated method') def get_read_db(self): """Return a database connection for read purposes :deprecated: See `trac.db.api.get_read_db` for detailed documentation. """ return BloodhoundConnectionWrapper(self.parent.get_read_db(), self) @property def db_query(self): """Return a context manager 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 ..."): ... If you don't need to manipulate the connection itself, this can even be simplified to:: for row in env.db_query("SELECT ..."): ... :warning: after a `with env.db_query 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`). """ return ProductEnvContextManager(QueryContextManager(self.parent), self) @property def db_transaction(self): """Return a context manager 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. Like for its read-only counterpart, you can directly execute a DML query on the `db`:: with env.db_transaction as db: db("UPDATE ...") If you don't need to manipulate the connection itself, this can also be simplified to:: env.db_transaction("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`). """ return ProductEnvContextManager(TransactionContextManager(self.parent), self) def shutdown(self, tid=None): """Close the environment.""" RepositoryManager(self).shutdown(tid) # FIXME: Shared DB so IMO this should not happen ... at least not here #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 create(self, options=[]): """Placeholder for compatibility when trying to create the basic directory structure of the environment, etc ... This method does nothing at all. """ # TODO: Handle options args def setup_config(self): """Load the configuration object. """ import trac.config parent_path = MultiProductSystem(self.parent).product_config_parent if parent_path and os.path.isfile(parent_path): parents = [trac.config.Configuration(parent_path)] else: parents = [self.parent.config] self.config = Configuration(self.parent, self.product.prefix, parents) self.setup_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 format = self.log_format self.parent.log.debug("Log type '%s' for product '%s'", logtype, self.product.prefix) # Force logger inheritance on identical configuration if (logtype, logfile, format) == (self.parent.log_type, self.parent.log_file, self.parent.log_format): logtype = 'inherit' if logtype == 'inherit': self.log = self.parent.log self._log_handler = self.parent._log_handler self.parent.log.warning( "Inheriting parent logger for product '%s'", self.product.prefix) else: if logtype == 'file' and not os.path.isabs(logfile): logfile = os.path.join(self.get_log_dir(), logfile) logid = 'Trac.%s.%s' % \ (sha1(self.parent.path).hexdigest(), self.product.prefix) 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, logid, format=format) from trac import core, __version__ as VERSION self.log.info( '-' * 32 + ' product %s environment startup [Trac %s] ' + '-' * 32, self.product.prefix, get_pkginfo(core).get('version', VERSION)) def needs_upgrade(self): """Return whether the environment needs to be upgraded.""" # Upgrades are handled by global environment 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 """ # Upgrades handled by global environment 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: urlpattern = MultiProductSystem(self.parent).product_base_url if not urlpattern: self.log.warn("product_base_url option not set in " "configuration, generated links may be " "incorrect") urlpattern = 'products/$(prefix)s' envname = os.path.basename(self.parent.path) prefix = unicode_quote(self.product.prefix, safe="") name = unicode_quote(self.product.name, safe="") url = urlpattern.replace('$(', '%(') \ .replace('%(envname)s', envname) \ .replace('%(prefix)s', prefix) \ .replace('%(name)s', name) if urlsplit(url).netloc: # Absolute URLs _abs_href = Href(url) else: # Relative URLs parent_href = Href(self.parent.abs_href(), path_safe="/!~*'()%", query_safe="!~*'()%") _abs_href = Href(parent_href(url)) else: _abs_href = Href(self.base_url) return _abs_href # Multi-product API extensions @classmethod def lookup_global_env(cls, env): return env.parent if isinstance(env, ProductEnvironment) else env @classmethod def lookup_env(cls, env, prefix=None, name=None): """Instantiate environment according to product prefix or name @throws LookupError if no product matches neither prefix nor name """ if isinstance(env, ProductEnvironment): global_env = env.parent else: global_env = env # FIXME: Update if multiproduct.dbcursor.GLOBAL_PRODUCT != '' if not prefix and not name: return global_env elif isinstance(env, ProductEnvironment) and \ env.product.prefix == prefix: return env if prefix: try: return ProductEnvironment(global_env, prefix) except LookupError: if not name: raise if name: # Lookup product by name products = Product.select(global_env, where={'name': name}) if products: return ProductEnvironment(global_env, products[0]) else: raise LookupError("Missing product '%s'" % (name, )) else: raise LookupError("Mising product '%s'" % (prefix or name, )) @classmethod def resolve_href(cls, to_env, at_env): """Choose absolute or relative href when generating links to a product (or global) environment. @param at_env: href expansion is taking place in the scope of this environment @param to_env: generated URLs point to resources in this environment """ at_href = at_env.abs_href() target_href = to_env.abs_href() if urlsplit(at_href)[1] == urlsplit(target_href)[1]: return to_env.href else: return to_env.abs_href
class InterTracDispatcher(Component): """InterTrac dispatcher.""" implements(IRequestHandler, IWikiMacroProvider) is_valid_default_handler = False intertrac_section = ConfigSection( 'intertrac', """This section configures InterTrac prefixes. Option names in this section that contain a `.` are of the format `<name>.<attribute>`. Option names that don't contain a `.` define an alias. The `.url` attribute is mandatory and is used for locating the other Trac. This can be a relative path when the other Trac environment is located on the same server. The `.title` attribute is used for generating a tooltip when the cursor is hovered over an InterTrac link. Example configuration: {{{#!ini [intertrac] # -- Example of setting up an alias: t = trac # -- Link to an external Trac: genshi.title = Edgewall's Trac for Genshi genshi.url = http://genshi.edgewall.org }}} """) # IRequestHandler methods def match_request(self, req): match = re.match(r'^/intertrac/(.*)', req.path_info) if match: if match.group(1): req.args['link'] = match.group(1) return True def process_request(self, req): link = req.args.get('link', '') parts = link.split(':', 1) if len(parts) > 1: resolver, target = parts if target[:1] + target[-1:] not in ('""', "''"): link = '%s:"%s"' % (resolver, target) from trac.web.chrome import web_context link_frag = extract_link(self.env, web_context(req), link) if isinstance(link_frag, (Element, Fragment)): elt = find_element(link_frag, 'href') if elt is None: raise TracError( _( "Can't view %(link)s. Resource doesn't exist or " "you don't have the required permission.", link=link)) href = elt.attrib.get('href') else: href = req.href(link.rstrip(':')) req.redirect(href) # IWikiMacroProvider methods def get_macros(self): yield 'InterTrac' def get_macro_description(self, name): return 'messages', N_("Provide a list of known InterTrac prefixes.") def expand_macro(self, formatter, name, content): intertracs = {} for key, value in self.intertrac_section.options(): idx = key.rfind('.') if idx > 0: # 0 itself doesn't help much: .xxx = ... prefix, attribute = key[:idx], key[idx + 1:] intertrac = intertracs.setdefault(prefix, {}) intertrac[attribute] = value else: intertracs[key] = value # alias if 'trac' not in intertracs: intertracs['trac'] = { 'title': _('The Trac Project'), 'url': 'http://trac.edgewall.org' } def generate_prefix(prefix): intertrac = intertracs[prefix] if isinstance(intertrac, basestring): yield tag.tr( tag.td(tag.strong(prefix)), tag.td( tag_("Alias for %(name)s", name=tag.strong(intertrac)))) else: url = intertrac.get('url', '') if url: title = intertrac.get('title', url) yield tag.tr( tag.td( tag.a(tag.strong(prefix), href=url + '/timeline')), tag.td(tag.a(title, href=url))) return tag.table(class_="wiki intertrac")( tag.tr(tag.th(tag.em(_("Prefix"))), tag.th(tag.em(_("Trac Site")))), [generate_prefix(p) for p in sorted(intertracs.keys())])
class TicketSystem(Component): implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager, ITicketManipulator) change_listeners = ExtensionPoint(ITicketChangeListener) milestone_change_listeners = ExtensionPoint(IMilestoneChangeListener) realm = 'ticket' ticket_custom_section = ConfigSection( 'ticket-custom', """In this section, you can define additional fields for tickets. See TracTicketsCustomFields for more details.""") action_controllers = OrderedExtensionsOption( 'ticket', 'workflow', ITicketActionController, default='ConfigurableTicketWorkflow', include_missing=False, doc="""Ordered list of workflow controllers to use for ticket actions. """) restrict_owner = BoolOption( 'ticket', 'restrict_owner', 'false', """Make the owner field of tickets use a drop-down menu. Be sure to understand the performance implications before activating this option. See [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List]. Please note that e-mail addresses are '''not''' obfuscated in the resulting drop-down menu, so this option should not be used if e-mail addresses must remain protected. """) default_version = Option('ticket', 'default_version', '', """Default version for newly created tickets.""") default_type = Option('ticket', 'default_type', 'defect', """Default type for newly created tickets.""") default_priority = Option( 'ticket', 'default_priority', 'major', """Default priority for newly created tickets.""") default_milestone = Option( 'ticket', 'default_milestone', '', """Default milestone for newly created tickets.""") default_component = Option( 'ticket', 'default_component', '', """Default component for newly created tickets.""") default_severity = Option( 'ticket', 'default_severity', '', """Default severity for newly created tickets.""") default_summary = Option( 'ticket', 'default_summary', '', """Default summary (title) for newly created tickets.""") default_description = Option( 'ticket', 'default_description', '', """Default description for newly created tickets.""") default_keywords = Option( 'ticket', 'default_keywords', '', """Default keywords for newly created tickets.""") default_owner = Option( 'ticket', 'default_owner', '< default >', """Default owner for newly created tickets. The component owner is used when set to the value `< default >`. """) default_cc = Option('ticket', 'default_cc', '', """Default cc: list for newly created tickets.""") default_resolution = Option( 'ticket', 'default_resolution', 'fixed', """Default resolution for resolving (closing) tickets.""") allowed_empty_fields = ListOption( 'ticket', 'allowed_empty_fields', 'milestone, version', doc="""Comma-separated list of `select` fields that can have an empty value. (//since 1.1.2//)""") max_comment_size = IntOption( 'ticket', 'max_comment_size', 262144, """Maximum allowed comment size in characters.""") max_description_size = IntOption( 'ticket', 'max_description_size', 262144, """Maximum allowed description size in characters.""") max_summary_size = IntOption( 'ticket', 'max_summary_size', 262144, """Maximum allowed summary size in characters. (//since 1.0.2//)""") def __init__(self): self.log.debug('action controllers for ticket workflow: %r', [c.__class__.__name__ for c in self.action_controllers]) # Public API def get_available_actions(self, req, ticket): """Returns a sorted list of available actions""" # The list should not have duplicates. actions = {} for controller in self.action_controllers: weighted_actions = controller.get_ticket_actions(req, ticket) or [] for weight, action in weighted_actions: if action in actions: actions[action] = max(actions[action], weight) else: actions[action] = weight all_weighted_actions = [(weight, action) for action, weight in actions.items()] return [x[1] for x in sorted(all_weighted_actions, reverse=True)] def get_all_status(self): """Returns a sorted list of all the states all of the action controllers know about.""" valid_states = set() for controller in self.action_controllers: valid_states.update(controller.get_all_status() or []) return sorted(valid_states) def get_ticket_field_labels(self): """Produce a (name,label) mapping from `get_ticket_fields`.""" labels = {f['name']: f['label'] for f in self.get_ticket_fields()} labels['attachment'] = _("Attachment") return labels def get_ticket_fields(self): """Returns list of fields available for tickets. Each field is a dict with at least the 'name', 'label' (localized) and 'type' keys. It may in addition contain the 'custom' key, the 'optional' and the 'options' keys. When present 'custom' and 'optional' are always `True`. """ fields = copy.deepcopy(self.fields) label = 'label' # workaround gettext extraction bug for f in fields: f[label] = gettext(f[label]) return fields def reset_ticket_fields(self): """Invalidate ticket field cache.""" del self.fields @cached def fields(self): """Return the list of fields available for tickets.""" from trac.ticket import model fields = TicketFieldList() # Basic text fields fields.append({ 'name': 'summary', 'type': 'text', 'label': N_('Summary') }) fields.append({ 'name': 'reporter', 'type': 'text', 'label': N_('Reporter') }) # Owner field, by default text but can be changed dynamically # into a drop-down depending on configuration (restrict_owner=true) fields.append({'name': 'owner', 'type': 'text', 'label': N_('Owner')}) # Description fields.append({ 'name': 'description', 'type': 'textarea', 'format': 'wiki', 'label': N_('Description') }) # Default select and radio fields selects = [('type', N_('Type'), model.Type), ('status', N_('Status'), model.Status), ('priority', N_('Priority'), model.Priority), ('milestone', N_('Milestone'), model.Milestone), ('component', N_('Component'), model.Component), ('version', N_('Version'), model.Version), ('severity', N_('Severity'), model.Severity), ('resolution', N_('Resolution'), model.Resolution)] for name, label, cls in selects: options = [val.name for val in cls.select(self.env)] if not options: # Fields without possible values are treated as if they didn't # exist continue field = { 'name': name, 'type': 'select', 'label': label, 'value': getattr(self, 'default_' + name, ''), 'options': options } if name in ('status', 'resolution'): field['type'] = 'radio' field['optional'] = True elif name in self.allowed_empty_fields: field['optional'] = True fields.append(field) # Advanced text fields fields.append({ 'name': 'keywords', 'type': 'text', 'format': 'list', 'label': N_('Keywords') }) fields.append({ 'name': 'cc', 'type': 'text', 'format': 'list', 'label': N_('Cc') }) # Date/time fields fields.append({ 'name': 'time', 'type': 'time', 'format': 'relative', 'label': N_('Created') }) fields.append({ 'name': 'changetime', 'type': 'time', 'format': 'relative', 'label': N_('Modified') }) for field in self.custom_fields: if field['name'] in [f['name'] for f in fields]: self.log.warning('Duplicate field name "%s" (ignoring)', field['name']) continue fields.append(field) return fields reserved_field_names = [ 'report', 'order', 'desc', 'group', 'groupdesc', 'col', 'row', 'format', 'max', 'page', 'verbose', 'comment', 'or', 'id', 'time', 'changetime', 'owner', 'reporter', 'cc', 'summary', 'description', 'keywords' ] def get_custom_fields(self): return copy.deepcopy(self.custom_fields) @cached def custom_fields(self): """Return the list of custom ticket fields available for tickets.""" fields = TicketFieldList() config = self.ticket_custom_section for name in [ option for option, value in config.options() if '.' not in option ]: field = { 'name': name, 'custom': True, 'type': config.get(name), 'order': config.getint(name + '.order', 0), 'label': config.get(name + '.label') or name.replace("_", " ").strip().capitalize(), 'value': config.get(name + '.value', '') } if field['type'] == 'select' or field['type'] == 'radio': field['options'] = config.getlist(name + '.options', sep='|') if '' in field['options'] or \ field['name'] in self.allowed_empty_fields: field['optional'] = True if '' in field['options']: field['options'].remove('') elif field['type'] == 'checkbox': field['value'] = '1' if as_bool(field['value']) else '0' elif field['type'] == 'text': field['format'] = config.get(name + '.format', 'plain') field['max_size'] = config.getint(name + '.max_size', 0) elif field['type'] == 'textarea': field['format'] = config.get(name + '.format', 'plain') field['max_size'] = config.getint(name + '.max_size', 0) field['height'] = config.getint(name + '.rows') elif field['type'] == 'time': field['format'] = config.get(name + '.format', 'datetime') if field['name'] in self.reserved_field_names: self.log.warning( 'Field name "%s" is a reserved name ' '(ignoring)', field['name']) continue if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']): self.log.warning( 'Invalid name for custom field: "%s" ' '(ignoring)', field['name']) continue fields.append(field) fields.sort(key=lambda f: (f['order'], f['name'])) return fields def get_field_synonyms(self): """Return a mapping from field name synonyms to field names. The synonyms are supposed to be more intuitive for custom queries.""" # i18n TODO - translated keys return {'created': 'time', 'modified': 'changetime'} def eventually_restrict_owner(self, field, ticket=None): """Restrict given owner field to be a list of users having the TICKET_MODIFY permission (for the given ticket) """ if self.restrict_owner: field['type'] = 'select' field['options'] = self.get_allowed_owners(ticket) field['optional'] = True def get_allowed_owners(self, ticket=None): """Returns a list of permitted ticket owners (those possessing the TICKET_MODIFY permission). Returns `None` if the option `[ticket]` `restrict_owner` is `False`. If `ticket` is not `None`, fine-grained permission checks are used to determine the allowed owners for the specified resource. :since: 1.0.3 """ if self.restrict_owner: allowed_owners = [] for user in PermissionSystem(self.env) \ .get_users_with_permission('TICKET_MODIFY'): if not ticket or \ 'TICKET_MODIFY' in PermissionCache(self.env, user, ticket.resource): allowed_owners.append(user) allowed_owners.sort() return allowed_owners # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): # Validate select fields for known values. for field in ticket.fields: if 'options' not in field: continue name = field['name'] if name == 'status': continue if name in ticket and name in ticket._old: value = ticket[name] if value: if value not in field['options']: yield name, _('"%(value)s" is not a valid value', value=value) elif not field.get('optional', False): yield name, _("field cannot be empty") # Validate description length. if len(ticket['description'] or '') > self.max_description_size: yield 'description', _( "Must be less than or equal to %(num)s " "characters", num=self.max_description_size) # Validate summary length. if not ticket['summary']: yield 'summary', _("Tickets must contain a summary.") elif len(ticket['summary'] or '') > self.max_summary_size: yield 'summary', _( "Must be less than or equal to %(num)s " "characters", num=self.max_summary_size) # Validate custom field length. for field in ticket.custom_fields: field_attrs = ticket.fields.by_name(field) max_size = field_attrs.get('max_size', 0) if 0 < max_size < len(ticket[field] or ''): label = field_attrs.get('label') yield label or field, _( "Must be less than or equal to " "%(num)s characters", num=max_size) # Validate time field content. for field in ticket.time_fields: value = ticket[field] if field in ticket.custom_fields and \ field in ticket._old and \ not isinstance(value, datetime): field_attrs = ticket.fields.by_name(field) format = field_attrs.get('format') try: ticket[field] = user_time(req, parse_date, value, hint=format) \ if value else None except TracError as e: # Degrade TracError to warning. ticket[field] = value label = field_attrs.get('label') yield label or field, to_unicode(e) def validate_comment(self, req, comment): # Validate comment length if len(comment or '') > self.max_comment_size: yield _("Must be less than or equal to %(num)s characters", num=self.max_comment_size) # IPermissionRequestor methods def get_permission_actions(self): return [ 'TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP', 'TICKET_VIEW', 'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION', 'TICKET_EDIT_COMMENT', ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']), ('TICKET_ADMIN', [ 'TICKET_CREATE', 'TICKET_MODIFY', 'TICKET_VIEW', 'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION', 'TICKET_EDIT_COMMENT' ]) ] # IWikiSyntaxProvider methods def get_link_resolvers(self): return [('bug', self._format_link), ('issue', self._format_link), ('ticket', self._format_link), ('comment', self._format_comment_link)] def get_wiki_syntax(self): yield ( # matches #... but not &#... (HTML entity) r"!?(?<!&)#" # optional intertrac shorthand #T... + digits r"(?P<it_ticket>%s)%s" % (WikiParser.INTERTRAC_SCHEME, Ranges.RE_STR), lambda x, y, z: self._format_link(x, 'ticket', y[1:], y, z)) def _format_link(self, formatter, ns, target, label, fullmatch=None): intertrac = formatter.shorthand_intertrac_helper( ns, target, label, fullmatch) if intertrac: return intertrac try: link, params, fragment = formatter.split_link(target) r = Ranges(link) if len(r) == 1: num = r.a ticket = formatter.resource(self.realm, num) from trac.ticket.model import Ticket if Ticket.id_is_valid(num) and \ 'TICKET_VIEW' in formatter.perm(ticket): # TODO: attempt to retrieve ticket view directly, # something like: t = Ticket.view(num) for type, summary, status, resolution in \ self.env.db_query(""" SELECT type, summary, status, resolution FROM ticket WHERE id=%s """, (str(num),)): description = self.format_summary( summary, status, resolution, type) title = '#%s: %s' % (num, description) href = formatter.href.ticket(num) + params + fragment return tag.a(label, title=title, href=href, class_='%s ticket' % status) else: ranges = str(r) if params: params = '&' + params[1:] label_wrap = label.replace(',', u',\u200b') ranges_wrap = ranges.replace(',', u', ') return tag.a(label_wrap, title=_("Tickets %(ranges)s", ranges=ranges_wrap), href=formatter.href.query(id=ranges) + params) except ValueError: pass return tag.a(label, class_='missing ticket') def _format_comment_link(self, formatter, ns, target, label): resource = None if ':' in target: elts = target.split(':') if len(elts) == 3: cnum, realm, id = elts if cnum != 'description' and cnum and not cnum[0].isdigit(): realm, id, cnum = elts # support old comment: style id = as_int(id, None) if realm in ('bug', 'issue'): realm = 'ticket' resource = formatter.resource(realm, id) else: resource = formatter.resource cnum = target if resource and resource.id and resource.realm == self.realm and \ cnum and (cnum.isdigit() or cnum == 'description'): href = title = class_ = None if self.resource_exists(resource): from trac.ticket.model import Ticket ticket = Ticket(self.env, resource.id) if cnum != 'description' and not ticket.get_change(cnum): title = _("ticket comment does not exist") class_ = 'missing ticket' elif 'TICKET_VIEW' in formatter.perm(resource): href = formatter.href.ticket(resource.id) + \ "#comment:%s" % cnum if resource.id != formatter.resource.id: summary = self.format_summary(ticket['summary'], ticket['status'], ticket['resolution'], ticket['type']) if cnum == 'description': title = _("Description for #%(id)s: %(summary)s", id=resource.id, summary=summary) else: title = _( "Comment %(cnum)s for #%(id)s: " "%(summary)s", cnum=cnum, id=resource.id, summary=summary) class_ = ticket['status'] + ' ticket' else: title = _("Description") if cnum == 'description' \ else _("Comment %(cnum)s", cnum=cnum) class_ = 'ticket' else: title = _("no permission to view ticket") class_ = 'forbidden ticket' else: title = _("ticket does not exist") class_ = 'missing ticket' return tag.a(label, class_=class_, href=href, title=title) return label # IResourceManager methods def get_resource_realms(self): yield self.realm def get_resource_description(self, resource, format=None, context=None, **kwargs): if format == 'compact': return '#%s' % resource.id elif format == 'summary': from trac.ticket.model import Ticket ticket = Ticket(self.env, resource.id) args = [ ticket[f] for f in ('summary', 'status', 'resolution', 'type') ] return self.format_summary(*args) return _("Ticket #%(shortname)s", shortname=resource.id) def format_summary(self, summary, status=None, resolution=None, type=None): summary = shorten_line(summary) if type: summary = type + ': ' + summary if status: if status == 'closed' and resolution: status += ': ' + resolution return "%s (%s)" % (summary, status) else: return summary def resource_exists(self, resource): """ >>> from trac.test import EnvironmentStub >>> from trac.resource import Resource, resource_exists >>> env = EnvironmentStub() >>> resource_exists(env, Resource('ticket', 123456)) False >>> from trac.ticket.model import Ticket >>> t = Ticket(env) >>> int(t.insert()) 1 >>> resource_exists(env, t.resource) True """ try: id_ = int(resource.id) except (TypeError, ValueError): return False if self.env.db_query("SELECT id FROM ticket WHERE id=%s", (id_, )): if resource.version is None: return True revcount = self.env.db_query( """ SELECT count(DISTINCT time) FROM ticket_change WHERE ticket=%s """, (id_, )) return revcount[0][0] >= resource.version else: return False
class DefaultTicketGroupStatsProvider(Component): """Configurable ticket group statistics provider. Example configuration (which is also the default): {{{ [milestone-groups] # Definition of a 'closed' group: closed = closed # The definition consists in a comma-separated list of accepted status. # Also, '*' means any status and could be used to associate all remaining # states to one catch-all group. # Qualifiers for the above group (the group must have been defined first): closed.order = 0 # sequence number in the progress bar closed.query_args = group=resolution # optional extra param for the query closed.overall_completion = true # count for overall completion # Definition of an 'active' group: active = * # one catch-all group is allowed active.order = 1 active.css_class = open # CSS class for this interval active.label = in progress # Displayed name for the group, # needed for non-ascii group names # The CSS class can be one of: new (yellow), open (no color) or # closed (green). New styles can easily be added using the following # selector: `table.progress td.<class>` }}} """ implements(ITicketGroupStatsProvider) milestone_groups_section = ConfigSection( 'milestone-groups', """As the workflow for tickets is now configurable, there can be many ticket states, and simply displaying closed tickets vs. all the others is maybe not appropriate in all cases. This section enables one to easily create ''groups'' of states that will be shown in different colors in the milestone progress bar. Example configuration (the default only has closed and active): {{{ closed = closed # sequence number in the progress bar closed.order = 0 # optional extra param for the query (two additional columns: created and modified and sort on created) closed.query_args = group=resolution,order=time,col=id,col=summary,col=owner,col=type,col=priority,col=component,col=severity,col=time,col=changetime # indicates groups that count for overall completion percentage closed.overall_completion = true new = new new.order = 1 new.css_class = new new.label = new # one catch-all group is allowed active = * active.order = 2 # CSS class for this interval active.css_class = open # Displayed label for this group active.label = in progress }}} The definition consists in a comma-separated list of accepted status. Also, '*' means any status and could be used to associate all remaining states to one catch-all group. The CSS class can be one of: new (yellow), open (no color) or closed (green). New styles can easily be added using the following selector: `table.progress td.<class>` (''since 0.11'')""") default_milestone_groups = [{ 'name': 'closed', 'status': 'closed', 'query_args': 'group=resolution', 'overall_completion': 'true' }, { 'name': 'active', 'status': '*', 'css_class': 'open' }] def _get_ticket_groups(self): """Returns a list of dict describing the ticket groups in the expected order of appearance in the milestone progress bars. """ if 'milestone-groups' in self.config: groups = {} order = 0 for groupname, value in self.milestone_groups_section.options(): qualifier = 'status' if '.' in groupname: groupname, qualifier = groupname.split('.', 1) group = groups.setdefault(groupname, { 'name': groupname, 'order': order }) group[qualifier] = value order = max(order, int(group['order'])) + 1 return [ group for group in sorted(groups.values(), key=lambda g: int(g['order'])) ] else: return self.default_milestone_groups def get_ticket_group_stats(self, ticket_ids): total_cnt = len(ticket_ids) all_statuses = set(TicketSystem(self.env).get_all_status()) status_cnt = {} for s in all_statuses: status_cnt[s] = 0 if total_cnt: for status, count in self.env.db_query(""" SELECT status, count(status) FROM ticket WHERE id IN (%s) GROUP BY status """ % ",".join(str(x) for x in sorted(ticket_ids))): status_cnt[status] = count stat = TicketGroupStats(_('ticket status'), _('tickets')) remaining_statuses = set(all_statuses) groups = self._get_ticket_groups() catch_all_group = None # we need to go through the groups twice, so that the catch up group # doesn't need to be the last one in the sequence for group in groups: status_str = group['status'].strip() if status_str == '*': if catch_all_group: raise TracError( _( "'%(group1)s' and '%(group2)s' milestone groups " "both are declared to be \"catch-all\" groups. " "Please check your configuration.", group1=group['name'], group2=catch_all_group['name'])) catch_all_group = group else: group_statuses = set([s.strip() for s in status_str.split(',')]) \ & all_statuses if group_statuses - remaining_statuses: raise TracError( _( "'%(groupname)s' milestone group reused status " "'%(status)s' already taken by other groups. " "Please check your configuration.", groupname=group['name'], status=', '.join(group_statuses - remaining_statuses))) else: remaining_statuses -= group_statuses group['statuses'] = group_statuses if catch_all_group: catch_all_group['statuses'] = remaining_statuses for group in groups: group_cnt = 0 query_args = {} for s, cnt in status_cnt.iteritems(): if s in group['statuses']: group_cnt += cnt query_args.setdefault('status', []).append(s) for arg in [ kv for kv in group.get('query_args', '').split(',') if '=' in kv ]: k, v = [a.strip() for a in arg.split('=', 1)] query_args.setdefault(k, []).append(v) stat.add_interval(group.get('label', group['name']), group_cnt, query_args, group.get('css_class', group['name']), as_bool(group.get('overall_completion'))) stat.refresh_calcs() return stat
class RepositoryManager(Component): """Version control system manager.""" implements(IRequestFilter, IResourceManager, IRepositoryProvider) connectors = ExtensionPoint(IRepositoryConnector) providers = ExtensionPoint(IRepositoryProvider) change_listeners = ExtensionPoint(IRepositoryChangeListener) repositories_section = ConfigSection( 'repositories', """One of the alternatives for registering new repositories is to populate the `[repositories]` section of the `trac.ini`. This is especially suited for setting up convenience aliases, short-lived repositories, or during the initial phases of an installation. See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details about the format adopted for this section and the rest of that page for the other alternatives. (''since 0.12'')""") repository_type = Option( 'trac', 'repository_type', 'svn', """Default repository connector type. (''since 0.10'') This is also used as the default repository type for repositories defined in [[TracIni#repositories-section repositories]] or using the "Repositories" admin panel. (''since 0.12'') """) repository_dir = Option( 'trac', 'repository_dir', '', """Path to the default repository. This can also be a relative path (''since 0.11''). This option is deprecated, and repositories should be defined in the [TracIni#repositories-section repositories] section, or using the "Repositories" admin panel. (''since 0.12'')""") repository_sync_per_request = ListOption( 'trac', 'repository_sync_per_request', '(default)', doc="""List of repositories that should be synchronized on every page request. Leave this option empty if you have set up post-commit hooks calling `trac-admin $ENV changeset added` on all your repositories (recommended). Otherwise, set it to a comma-separated list of repository names. Note that this will negatively affect performance, and will prevent changeset listeners from receiving events from the repositories specified here. The default is to synchronize the default repository, for backward compatibility. (''since 0.12'')""") def __init__(self): self._cache = {} self._lock = threading.Lock() self._connectors = None self._all_repositories = None # IRequestFilter methods def pre_process_request(self, req, handler): from trac.web.chrome import Chrome, add_warning if handler is not Chrome(self.env): for reponame in self.repository_sync_per_request: start = time.time() if is_default(reponame): reponame = '' try: repo = self.get_repository(reponame) if repo: repo.sync() else: self.log.warning( "Unable to find repository '%s' for " "synchronization", reponame or '(default)') continue except TracError, e: add_warning( req, _( "Can't synchronize with repository \"%(name)s\" " "(%(error)s). Look in the Trac log for more " "information.", name=reponame or '(default)', error=to_unicode(e.message))) self.log.info("Synchronized '%s' repository in %0.2f seconds", reponame or '(default)', time.time() - start) return handler
class IniEditorBasicSecurityManager(Component): """ Reads the option restrictions from the `trac.ini`. They're read from the section `[ini-editor-restrictions]`. Each option is defined as `<section-name>|<option-name>` and the value is either 'hidden' (option can neither be seen nor changed), 'readonly' (option can be seen but not changed), or 'modifiable' (option can be seen and changed). Section-wide access can be specified by `<section-name>|*`. The default value for options not specified can be set by `default-access` in `[ini-editor-restrictions]`. Setting it to `modifiable` results in specifying a "black-list", setting it to one of the other two values resuls in specifying a "white-list". """ implements(IOptionSecurityManager) DEFAULT_RESTRICTIONS = { 'ini-editor': { '*': ACCESS_READONLY, 'password-options': ACCESS_MODIFIABLE }, 'ini-editor-restrictions': { '*': ACCESS_READONLY }, 'trac': { 'database': ACCESS_HIDDEN # <- may contain the database password } } choices = [ACCESS_READONLY, ACCESS_HIDDEN, ACCESS_MODIFIABLE] default_access = ChoiceOption( 'ini-editor-restrictions', 'default-access', choices, doc="""Defines the default access level for options that don't have an explicit access level defined. Defaults to readonly.""", doc_domain="iniadminpanel") ini_section = ConfigSection( 'ini-editor-restrictions', """This section is used to store restriction configurations used by TracIniAdminPanel plugin security manager. An example file can be found at http://trac-hacks.org/browser/traciniadminpanelplugin/0.12/safe-restrictions.ini""", doc_domain='iniadminpanel') def __init__(self): restrictions = self.config.options('ini-editor-restrictions') self.restrictions = copy.deepcopy(self.DEFAULT_RESTRICTIONS) for restriction_on, level in restrictions: if restriction_on == 'default-access': continue # NOTE: A dot seems to be a valid character in a option name (see # [ticket] -> commit_ticket_update_commands.close). A colon (':') is # considered an assignment operator. So we use the pipe ('|') char in # the hopes that it won't be used anywhere else. But to be on the safe # side we allow it in option names. parts = restriction_on.split('|', 2) if len(parts) < 2: self.log.warning('Invalid restriction name: ' + restriction_on) continue # no pipes in this name; so this is no valid restriction name. # Note that the name may contain more than one pipe if the # option name contains pipe chars. if level != ACCESS_HIDDEN and level != ACCESS_READONLY and level != ACCESS_MODIFIABLE: self.log.warning('Invalid restriction level for ' + restriction_on + ': ' + level) continue if parts[0] not in self.restrictions: self.restrictions[parts[0]] = {parts[1]: level} else: self.restrictions[parts[0]][parts[1]] = level def get_option_access(self, section_name, option_name): """Returns the access status for this option. Must return one of ACCESS_HIDDEN (option can neither be seen nor changed), ACCESS_READONLY (option can be seen but not changed), or ACCESS_MODIFIABLE (option can be seen and changed). """ section_restrictions = self.restrictions.get(section_name.lower(), None) if section_restrictions is None: return self.default_access # Return access level with fallbacks return section_restrictions.get( option_name, section_restrictions.get('*', self.default_access)) def is_value_valid(self, section_name, option_name, option_value): """Checks whether the specified value is valid for the specified option. This can be used for example to restrict system paths to a certain parent directory. Will also be used against default values, if they're to be used. Returns `True` (valid) or `False` (invalid) as first return value and a (possibly empty) string as second return value containing the reason why this value is invalid. Note that this method is only called if the user has actually write permissions to this option. Note also that this method is only called for changed values. So if the `trac.ini` already contains invalid values, then they won't be checked. """ return True, None
def expand_macro(self, formatter, name, content): from trac.config import ConfigSection, Option args, kw = parse_args(content) filters = {} for name, index in (('section', 0), ('option', 1)): pattern = kw.get(name, '').strip() if pattern: filters[name] = fnmatch.translate(pattern) continue prefix = args[index].strip() if index < len(args) else '' if prefix: filters[name] = re.escape(prefix) has_option_filter = 'option' in filters for name in ('section', 'option'): filters[name] = re.compile(filters[name], re.IGNORECASE).match \ if name in filters \ else lambda v: True section_filter = filters['section'] option_filter = filters['option'] section_registry = ConfigSection.get_registry(self.compmgr) option_registry = Option.get_registry(self.compmgr) options = {} for (section, key), option in option_registry.iteritems(): if section_filter(section) and option_filter(key): options.setdefault(section, {})[key] = option if not has_option_filter: for section in section_registry: if section_filter(section): options.setdefault(section, {}) for section in options: options[section] = sorted(options[section].itervalues(), key=lambda option: option.name) sections = [(section, section_registry[section].doc if section in section_registry else '') for section in sorted(options)] def default_cell(option): default = option.default if default is not None and default != '': return tag.td(tag.code(option.dumps(default)), class_='default') else: return tag.td(_("(no default)"), class_='nodefault') def options_table(section, options): if options: return tag.table(class_='wiki')( tag.tbody( tag.tr( tag.td(tag.a(tag.code(option.name), class_='tracini-option', href='#%s-%s-option' % (section, option.name))), tag.td(format_to_html(self.env, formatter.context, option.doc)), default_cell(option), id='%s-%s-option' % (section, option.name), class_='odd' if idx % 2 else 'even') for idx, option in enumerate(options))) return tag.div(class_='tracini')( (tag.h3(tag.code('[%s]' % section), id='%s-section' % section), format_to_html(self.env, formatter.context, section_doc), options_table(section, options.get(section))) for section, section_doc in sections)
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 webadmin.* = enabled }}} The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `webadmin` 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. (''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.log = None self.config = None # System info should be provided through ISystemInfoProvider rather # than appending to systeminfo, which may be a private in a future # release. self.systeminfo = [] 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 def get_configinfo(self): """Returns a list of dictionaries containing the `name` and `options` of each configuration section. The value of `options` is a list of dictionaries containing the `name`, `value` and `modified` state of each configuration option. The `modified` value is True if the value differs from its default. :since: version 1.1.2 """ defaults = self.config.defaults(self.compmgr) sections = [] for section in self.config.sections(self.compmgr): options = [] default_options = defaults.get(section, {}) for name, value in self.config.options(section, self.compmgr): default = default_options.get(name) or '' options.append({ 'name': name, 'value': value, 'modified': unicode(value) != unicode(default) }) options.sort(key=lambda o: o['name']) sections.append({'name': section, 'options': options}) sections.sort(key=lambda s: s['name']) return sections # 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__ 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(): if name.endswith('.*'): name = name[:-2] _rules[name.lower()] = value.lower() in ('enabled', 'on') 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) # 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 except # trac.test are enabled return component_name.startswith('trac.') and \ not component_name.startswith('trac.test.') 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.""" try: tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0] if tag != _VERSION: raise Exception("Unknown Trac environment type '%s'" % tag) except Exception as e: raise TracError("No Trac environment found at %s\n%s" % (self.path, e)) def get_db_cnx(self): """Return a database connection from the connection pool :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead `db_transaction` for obtaining the `db` database connection which can be used for performing any query (SELECT/INSERT/UPDATE/DELETE):: with env.db_transaction as db: ... Note that within the block, you don't need to (and shouldn't) call ``commit()`` yourself, the context manager will take care of it (if it's the outermost such context manager on the stack). `db_query` for obtaining a `db` database connection which can be used for performing SELECT queries only:: with env.db_query as db: ... """ return DatabaseManager(self).get_connection() @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() def with_transaction(self, db=None): """Decorator for transaction functions :deprecated:""" return with_transaction(self, db) def get_read_db(self): """Return a database connection for read purposes :deprecated: See `trac.db.api.get_read_db` for detailed documentation.""" return DatabaseManager(self).get_connection(readonly=True) @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.""" 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'), _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(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, 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 """ rows = self.db_query(""" SELECT value FROM system WHERE name='%sdatabase_version' """ % ('initial_' if initial else '')) return int(rows[0][0]) if rows else False def setup_config(self): """Load the configuration file.""" self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'), {'envname': os.path.basename(self.path)}) 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 logid = 'Trac.%s' % sha1(self.path).hexdigest() 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, logid, 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): """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. """ for username, name, email in 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 """): 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.""" for participant in self.setup_participants: args = () with self.db_query as db: if arity(participant.environment_needs_upgrade) == 1: args = (db,) if participant.environment_needs_upgrade(*args): self.log.warn("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: args = () with self.db_query as db: if arity(participant.environment_needs_upgrade) == 1: args = (db,) if participant.environment_needs_upgrade(*args): 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("%s.%s upgrading...", participant.__module__, participant.__class__.__name__) args = () with self.db_transaction as db: if arity(participant.upgrade_environment) == 1: args = (db,) participant.upgrade_environment(*args) # Database schema may have changed, so close all connections DatabaseManager(self).shutdown() 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.warn("base_url option not set in configuration, " "generated links may be incorrect") _abs_href = Href('') else: _abs_href = Href(self.base_url) return _abs_href
class Smileys(Component): """Replace smiley characters like `:-)` with icons. Smiley characters and icons are configurable in the `[wikiextras-smileys]` section in `trac.ini`. Use the `ShowSmileys` macro to display a list of currently defined smileys. """ implements(IWikiMacroProvider, IWikiSyntaxProvider) smileys_section = ConfigSection( 'wikiextras-smileys', """The set of smileys is configurable by providing associations between icon names and wiki keywords. A default set of icons and keywords is defined, which can be revoked one-by-one (_remove) or all at once (_remove_defaults). Example: {{{ [wikiextras-smileys] _remove_defaults = true _remove = :-( :( smiley = :-) :) smiley-wink = ;-) ;) clock = (CLOCK) (TIME) calendar-month = (CALENDAR) (DATE) chart = (CHART) document-excel = (EXCEL) document-word = (WORD) eye = (EYE) new = (NEW) tick = (TICK) }}} Keywords are space-separated! A smiley can also be removed by associating its icon with nothing: {{{ smiley = }}} Use the `ShowSmileys` macro to find out the current set of icons and keywords. """) remove_defaults = BoolOption('wikiextras-smileys', '_remove_defaults', False, doc="Set to true to remove all " "default smileys.") remove = ListOption('wikiextras-smileys', '_remove', sep=' ', doc="""\ Space-separated(!) list of keywords that shall not be interpreted as smileys (even if defined in this section).""") def __init__(self): self.smileys = None # IWikiSyntaxProvider methods def get_wiki_syntax(self): if self.smileys is None: self.smileys = SMILEYS.copy() if self.remove_defaults: self.smileys = {} for icon_name, value in self.smileys_section.options(): if not icon_name.startswith('_remove'): icon_file = icon_name if not icon_file.endswith('.png'): icon_file = '%s.png' % icon_file if value: for keyword in value.split(): self.smileys[keyword.strip()] = icon_file else: # no keyword, remove all smileys associated with icon for k in self.smileys.keys(): if self.smileys[k] == icon_file: del self.smileys[k] for keyword in self.remove: if keyword in self.smileys: del self.smileys[keyword] if self.smileys: yield (r"(?<!\w)!?(?:%s)" % prepare_regexp(self.smileys), self._format_smiley) else: yield (None, None) def get_link_resolvers(self): return [] #noinspection PyUnusedLocal def _format_smiley(self, formatter, match, fullmatch=None): #noinspection PyArgumentList loc = Icons(self.env).icon_location() return tag.img(src=formatter.href.chrome(loc[0], self.smileys[match]), alt=match, style="vertical-align: text-bottom") # IWikiMacroProvider methods def get_macros(self): yield 'ShowSmileys' #noinspection PyUnusedLocal def get_macro_description(self, name): return cleandoc("""Renders in a table the list of available smileys. Optional argument is the number of columns in the table (defaults 3). Comment: Prefixing a character sequence with `!` prevents it from being interpreted as a smiley. """) #noinspection PyUnusedLocal def expand_macro(self, formatter, name, content, args=None): # Merge smileys for presentation # First collect wikitexts for each unique filename syelims = {} # key=filename, value=wikitext for wikitext, filename in self.smileys.iteritems(): if filename not in syelims: syelims[filename] = [wikitext] else: syelims[filename].append(wikitext) # Reverse smileys = {} for filename, wikitexts in syelims.iteritems(): wikitexts.sort() smileys[' '.join(wikitexts)] = filename return render_table( smileys.keys(), content, lambda s: self._format_smiley(formatter, s.split(' ', 1)[0]))
class RepositoryManager(Component): """Version control system manager.""" implements(IRequestFilter, IResourceManager, IRepositoryProvider) connectors = ExtensionPoint(IRepositoryConnector) providers = ExtensionPoint(IRepositoryProvider) change_listeners = ExtensionPoint(IRepositoryChangeListener) repositories_section = ConfigSection( 'repositories', """One of the alternatives for registering new repositories is to populate the `[repositories]` section of the `trac.ini`. This is especially suited for setting up convenience aliases, short-lived repositories, or during the initial phases of an installation. See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details about the format adopted for this section and the rest of that page for the other alternatives. (''since 0.12'')""") repository_type = Option( 'trac', 'repository_type', 'svn', """Default repository connector type. (''since 0.10'') This is also used as the default repository type for repositories defined in [[TracIni#repositories-section repositories]] or using the "Repositories" admin panel. (''since 0.12'') """) repository_dir = Option( 'trac', 'repository_dir', '', """Path to the default repository. This can also be a relative path (''since 0.11''). This option is deprecated, and repositories should be defined in the [TracIni#repositories-section repositories] section, or using the "Repositories" admin panel. (''since 0.12'')""") repository_sync_per_request = ListOption( 'trac', 'repository_sync_per_request', '(default)', doc="""List of repositories that should be synchronized on every page request. Leave this option empty if you have set up post-commit hooks calling `trac-admin $ENV changeset added` on all your repositories (recommended). Otherwise, set it to a comma-separated list of repository names. Note that this will negatively affect performance, and will prevent changeset listeners from receiving events from the repositories specified here. (''since 0.12'')""") def __init__(self): self._cache = {} self._lock = threading.Lock() self._connectors = None self._all_repositories = None # IRequestFilter methods def pre_process_request(self, req, handler): from trac.web.chrome import Chrome, add_warning if handler is not Chrome(self.env): for reponame in self.repository_sync_per_request: start = time.time() if is_default(reponame): reponame = '' try: repo = self.get_repository(reponame) if repo: repo.sync() else: self.log.warning( "Unable to find repository '%s' for " "synchronization", reponame or '(default)') continue except TracError as e: add_warning( req, _( "Can't synchronize with repository \"%(name)s\" " "(%(error)s). Look in the Trac log for more " "information.", name=reponame or '(default)', error=to_unicode(e))) except Exception as e: add_warning( req, _( "Failed to sync with repository \"%(name)s\": " "%(error)s; repository information may be out of " "date. Look in the Trac log for more information " "including mitigation strategies.", name=reponame or '(default)', error=to_unicode(e))) self.log.error( "Failed to sync with repository \"%s\"; You may be " "able to reduce the impact of this issue by " "configuring [trac] repository_sync_per_request; see " "http://trac.edgewall.org/wiki/TracRepositoryAdmin" "#ExplicitSync for more detail: %s", reponame or '(default)', exception_to_unicode(e, traceback=True)) self.log.info("Synchronized '%s' repository in %0.2f seconds", reponame or '(default)', time.time() - start) return handler def post_process_request(self, req, template, data, content_type): return (template, data, content_type) # IResourceManager methods def get_resource_realms(self): yield 'changeset' yield 'source' yield 'repository' def get_resource_description(self, resource, format=None, **kwargs): if resource.realm == 'changeset': parent = resource.parent reponame = parent and parent.id id = resource.id if reponame: return _("Changeset %(rev)s in %(repo)s", rev=id, repo=reponame) else: return _("Changeset %(rev)s", rev=id) elif resource.realm == 'source': parent = resource.parent reponame = parent and parent.id id = resource.id version = '' if format == 'summary': repos = self.get_repository(reponame) node = repos.get_node(resource.id, resource.version) if node.isdir: kind = _("directory") elif node.isfile: kind = _("file") if resource.version: version = _(" at version %(rev)s", rev=resource.version) else: kind = _("path") if resource.version: version = '@%s' % resource.version in_repo = _(" in %(repo)s", repo=reponame) if reponame else '' # TRANSLATOR: file /path/to/file.py at version 13 in reponame return _('%(kind)s %(id)s%(at_version)s%(in_repo)s', kind=kind, id=id, at_version=version, in_repo=in_repo) elif resource.realm == 'repository': if not resource.id: return _("Default repository") return _("Repository %(repo)s", repo=resource.id) def get_resource_url(self, resource, href, **kwargs): if resource.realm == 'changeset': parent = resource.parent return href.changeset(resource.id, parent and parent.id or None) elif resource.realm == 'source': parent = resource.parent return href.browser(parent and parent.id or None, resource.id, rev=resource.version or None) elif resource.realm == 'repository': return href.browser(resource.id or None) def resource_exists(self, resource): if resource.realm == 'repository': reponame = resource.id else: reponame = resource.parent.id repos = self.env.get_repository(reponame) if not repos: return False if resource.realm == 'changeset': try: repos.get_changeset(resource.id) return True except NoSuchChangeset: return False elif resource.realm == 'source': try: repos.get_node(resource.id, resource.version) return True except NoSuchNode: return False elif resource.realm == 'repository': return True # IRepositoryProvider methods def get_repositories(self): """Retrieve repositories specified in TracIni. The `[repositories]` section can be used to specify a list of repositories. """ repositories = self.repositories_section reponames = {} # eventually add pre-0.12 default repository if self.repository_dir: reponames[''] = {'dir': self.repository_dir} # first pass to gather the <name>.dir entries for option in repositories: if option.endswith('.dir'): reponames[option[:-4]] = {} # second pass to gather aliases for option in repositories: alias = repositories.get(option) if '.' not in option: # Support <alias> = <repo> syntax option += '.alias' if option.endswith('.alias') and alias in reponames: reponames.setdefault(option[:-6], {})['alias'] = alias # third pass to gather the <name>.<detail> entries for option in repositories: if '.' in option: name, detail = option.rsplit('.', 1) if name in reponames and detail != 'alias': reponames[name][detail] = repositories.get(option) for reponame, info in reponames.iteritems(): yield (reponame, info) # Public API methods def get_supported_types(self): """Return the list of supported repository types.""" types = set(type_ for connector in self.connectors for (type_, prio) in connector.get_supported_types() or [] if prio >= 0) return list(types) def get_repositories_by_dir(self, directory): """Retrieve the repositories based on the given directory. :param directory: the key for identifying the repositories. :return: list of `Repository` instances. """ directory = os.path.join(os.path.normcase(directory), '') repositories = [] for reponame, repoinfo in self.get_all_repositories().iteritems(): dir = repoinfo.get('dir') if dir: dir = os.path.join(os.path.normcase(dir), '') if dir.startswith(directory): repos = self.get_repository(reponame) if repos: repositories.append(repos) return repositories def get_repository_id(self, reponame): """Return a unique id for the given repository name. This will create and save a new id if none is found. Note: this should probably be renamed as we're dealing exclusively with *db* repository ids here. """ with self.env.db_transaction as db: for id, in db( "SELECT id FROM repository WHERE name='name' AND value=%s", (reponame, )): return id id = db("SELECT COALESCE(MAX(id), 0) FROM repository")[0][0] + 1 db("INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)", (id, 'name', reponame)) return id def get_repository(self, reponame): """Retrieve the appropriate `Repository` for the given repository name. :param reponame: the key for specifying the repository. If no name is given, take the default repository. :return: if no corresponding repository was defined, simply return `None`. """ reponame = reponame or '' repoinfo = self.get_all_repositories().get(reponame, {}) if 'alias' in repoinfo: reponame = repoinfo['alias'] repoinfo = self.get_all_repositories().get(reponame, {}) rdir = repoinfo.get('dir') if not rdir: return None rtype = repoinfo.get('type') or self.repository_type # get a Repository for the reponame (use a thread-level cache) with self.env.db_transaction: # prevent possible deadlock, see #4465 with self._lock: tid = threading._get_ident() if tid in self._cache: repositories = self._cache[tid] else: repositories = self._cache[tid] = {} repos = repositories.get(reponame) if not repos: if not os.path.isabs(rdir): rdir = os.path.join(self.env.path, rdir) connector = self._get_connector(rtype) repos = connector.get_repository(rtype, rdir, repoinfo.copy()) repositories[reponame] = repos return repos def get_repository_by_path(self, path): """Retrieve a matching `Repository` for the given `path`. :param path: the eventually scoped repository-scoped path :return: a `(reponame, repos, path)` triple, where `path` is the remaining part of `path` once the `reponame` has been truncated, if needed. """ matches = [] path = path.strip('/') + '/' if path else '/' for reponame in self.get_all_repositories().keys(): stripped_reponame = reponame.strip('/') + '/' if path.startswith(stripped_reponame): matches.append((len(stripped_reponame), reponame)) if matches: matches.sort() length, reponame = matches[-1] path = path[length:] else: reponame = '' return (reponame, self.get_repository(reponame), path.rstrip('/') or '/') def get_default_repository(self, context): """Recover the appropriate repository from the current context. Lookup the closest source or changeset resource in the context hierarchy and return the name of its associated repository. """ while context: if context.resource.realm in ('source', 'changeset'): return context.resource.parent.id context = context.parent def get_all_repositories(self): """Return a dictionary of repository information, indexed by name.""" if not self._all_repositories: all_repositories = {} for provider in self.providers: for reponame, info in provider.get_repositories() or []: if reponame in all_repositories: self.log.warn("Discarding duplicate repository '%s'", reponame) else: info['name'] = reponame if 'id' not in info: info['id'] = self.get_repository_id(reponame) all_repositories[reponame] = info self._all_repositories = all_repositories return self._all_repositories def get_real_repositories(self): """Return a set of all real repositories (i.e. excluding aliases).""" repositories = set() for reponame in self.get_all_repositories(): try: repos = self.get_repository(reponame) if repos is not None: repositories.add(repos) except TracError: pass # Skip invalid repositories return repositories def reload_repositories(self): """Reload the repositories from the providers.""" with self._lock: # FIXME: trac-admin doesn't reload the environment self._cache = {} self._all_repositories = None self.config.touch() # Force environment reload def notify(self, event, reponame, revs): """Notify repositories and change listeners about repository events. The supported events are the names of the methods defined in the `IRepositoryChangeListener` interface. """ self.log.debug("Event %s on repository '%s' for changesets %r", event, reponame or '(default)', revs) # Notify a repository by name, and all repositories with the same # base, or all repositories by base or by repository dir repos = self.get_repository(reponame) repositories = [] if repos: base = repos.get_base() else: dir = os.path.abspath(reponame) repositories = self.get_repositories_by_dir(dir) if repositories: base = None else: base = reponame if base: repositories = [ r for r in self.get_real_repositories() if r.get_base() == base ] if not repositories: self.log.warn("Found no repositories matching '%s' base.", base or reponame) return for repos in sorted(repositories, key=lambda r: r.reponame): repos.sync() for rev in revs: args = [] if event == 'changeset_modified': args.append(repos.sync_changeset(rev)) try: changeset = repos.get_changeset(rev) except NoSuchChangeset: try: repos.sync_changeset(rev) changeset = repos.get_changeset(rev) except NoSuchChangeset: self.log.debug( "No changeset '%s' found in repository '%s'. " "Skipping subscribers for event %s", rev, repos.reponame or '(default)', event) continue self.log.debug("Event %s on repository '%s' for revision '%s'", event, repos.reponame or '(default)', rev) for listener in self.change_listeners: getattr(listener, event)(repos, changeset, *args) def shutdown(self, tid=None): """Free `Repository` instances bound to a given thread identifier""" if tid: assert tid == threading._get_ident() with self._lock: repositories = self._cache.pop(tid, {}) for reponame, repos in repositories.iteritems(): repos.close() # private methods def _get_connector(self, rtype): """Retrieve the appropriate connector for the given repository type. Note that the self._lock must be held when calling this method. """ if self._connectors is None: # build an environment-level cache for the preferred connectors self._connectors = {} for connector in self.connectors: for type_, prio in connector.get_supported_types() or []: keep = (connector, prio) if type_ in self._connectors and \ prio <= self._connectors[type_][1]: keep = None if keep: self._connectors[type_] = keep if rtype in self._connectors: connector, prio = self._connectors[rtype] if prio >= 0: # no error condition return connector else: raise TracError( _( 'Unsupported version control system "%(name)s"' ': %(error)s', name=rtype, error=to_unicode(connector.error))) else: raise TracError( _( 'Unsupported version control system "%(name)s": ' 'Can\'t find an appropriate component, maybe the ' 'corresponding plugin was not enabled? ', name=rtype))
class RequestDispatcher(Component): """Web request dispatcher. This component dispatches incoming requests to registered handlers. It also takes care of user authentication and request pre- and post-processing. """ required = True implements(ITemplateProvider) authenticators = ExtensionPoint(IAuthenticator) handlers = ExtensionPoint(IRequestHandler) filters = OrderedExtensionsOption('trac', 'request_filters', IRequestFilter, doc="""Ordered list of filters to apply to all requests.""") default_handler = ExtensionOption('trac', 'default_handler', IRequestHandler, 'WikiModule', """Name of the component that handles requests to the base URL. Options include `TimelineModule`, `RoadmapModule`, `BrowserModule`, `QueryModule`, `ReportModule`, `TicketModule` and `WikiModule`. The [/prefs/userinterface session preference] for default handler take precedence, when set. """) default_timezone = Option('trac', 'default_timezone', '', """The default timezone to use""") default_language = Option('trac', 'default_language', '', """The preferred language to use if no user preference has been set. """) default_date_format = ChoiceOption('trac', 'default_date_format', ('', 'iso8601'), """The date format. Valid options are 'iso8601' for selecting ISO 8601 format, or leave it empty which means the default date format will be inferred from the browser's default language. (''since 1.0'') """) use_xsendfile = BoolOption('trac', 'use_xsendfile', 'false', """When true, send a `X-Sendfile` header and no content when sending files from the filesystem, so that the web server handles the content. This requires a web server that knows how to handle such a header, like Apache with `mod_xsendfile` or lighttpd. (''since 1.0'') """) xsendfile_header = Option('trac', 'xsendfile_header', 'X-Sendfile', """The header to use if `use_xsendfile` is enabled. If Nginx is used, set `X-Accel-Redirect`. (''since 1.0.6'')""") configurable_headers = ConfigSection('http-headers', """ Headers to be added to the HTTP request. (''since 1.2.3'') The header name must conform to RFC7230 and the following reserved names are not allowed: content-type, content-length, location, etag, pragma, cache-control, expires. """) # Public API def authenticate(self, req): for authenticator in self.authenticators: try: authname = authenticator.authenticate(req) except TracError as e: self.log.error("Can't authenticate using %s: %s", authenticator.__class__.__name__, exception_to_unicode(e, traceback=True)) add_warning(req, _("Authentication error. " "Please contact your administrator.")) break # don't fallback to other authenticators if authname: return authname return 'anonymous' def dispatch(self, req): """Find a registered handler that matches the request and let it process it. In addition, this method initializes the data dictionary passed to the the template and adds the web site chrome. """ self.log.debug('Dispatching %r', req) chrome = Chrome(self.env) try: # Select the component that should handle the request chosen_handler = None for handler in self._request_handlers.values(): if handler.match_request(req): chosen_handler = handler break if not chosen_handler and req.path_info in ('', '/'): chosen_handler = self._get_valid_default_handler(req) # pre-process any incoming request, whether a handler # was found or not self.log.debug("Chosen handler is %s", chosen_handler) chosen_handler = self._pre_process_request(req, chosen_handler) if not chosen_handler: if req.path_info.endswith('/'): # Strip trailing / and redirect target = unicode_quote(req.path_info.rstrip('/')) if req.query_string: target += '?' + req.query_string req.redirect(req.href + target, permanent=True) raise HTTPNotFound('No handler matched request to %s', req.path_info) req.callbacks['chrome'] = partial(chrome.prepare_request, handler=chosen_handler) # Protect against CSRF attacks: we validate the form token # for all POST requests with a content-type corresponding # to form submissions if req.method == 'POST': ctype = req.get_header('Content-Type') if ctype: ctype, options = cgi.parse_header(ctype) if ctype in ('application/x-www-form-urlencoded', 'multipart/form-data') and \ req.args.get('__FORM_TOKEN') != req.form_token: if self.env.secure_cookies and req.scheme == 'http': msg = _('Secure cookies are enabled, you must ' 'use https to submit forms.') else: msg = _('Do you have cookies enabled?') raise HTTPBadRequest(_('Missing or invalid form token.' ' %(msg)s', msg=msg)) # Process the request and render the template resp = chosen_handler.process_request(req) if resp: template, data, metadata = \ self._post_process_request(req, *resp) if 'hdfdump' in req.args: req.perm.require('TRAC_ADMIN') # debugging helper - no need to render first out = io.BytesIO() pprint({'template': template, 'metadata': metadata, 'data': data}, out) req.send(out.getvalue(), 'text/plain') self.log.debug("Rendering response with template %s", template) metadata.setdefault('iterable', chrome.use_chunked_encoding) content_type = metadata.get('content_type') output = chrome.render_template(req, template, data, metadata) req.send(output, content_type or 'text/html') else: self.log.debug("Empty or no response from handler. " "Entering post_process_request.") self._post_process_request(req) except RequestDone: raise except Exception as e: # post-process the request in case of errors err = sys.exc_info() try: self._post_process_request(req) except RequestDone: raise except TracError as e2: self.log.warning("Exception caught while post-processing" " request: %s", exception_to_unicode(e2)) except Exception as e2: if not (type(e) is type(e2) and e.args == e2.args): self.log.error("Exception caught while post-processing" " request: %s", exception_to_unicode(e2, traceback=True)) if isinstance(e, PermissionError): raise HTTPForbidden(e) if isinstance(e, ResourceNotFound): raise HTTPNotFound(e) if isinstance(e, NotImplementedError): tb = traceback.extract_tb(err[2])[-1] self.log.warning("%s caught from %s:%d in %s: %s", e.__class__.__name__, tb[0], tb[1], tb[2], to_unicode(e) or "(no message)") raise HTTPInternalServerError(TracNotImplementedError(e)) if isinstance(e, TracError): raise HTTPInternalServerError(e) raise err[0], err[1], err[2] # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): return [pkg_resources.resource_filename('trac.web', 'templates')] # Internal methods def set_default_callbacks(self, req): """Setup request callbacks for lazily-evaluated properties. """ req.callbacks.update({ 'authname': self.authenticate, 'chrome': Chrome(self.env).prepare_request, 'form_token': self._get_form_token, 'lc_time': self._get_lc_time, 'locale': self._get_locale, 'perm': self._get_perm, 'session': self._get_session, 'tz': self._get_timezone, 'use_xsendfile': self._get_use_xsendfile, 'xsendfile_header': self._get_xsendfile_header, 'configurable_headers': self._get_configurable_headers, }) @lazy def _request_handlers(self): return {handler.__class__.__name__: handler for handler in self.handlers} def _get_valid_default_handler(self, req): # Use default_handler from the Session if it is a valid value. name = req.session.get('default_handler') handler = self._request_handlers.get(name) if handler and not is_valid_default_handler(handler): handler = None if not handler: # Use default_handler from project configuration. handler = self.default_handler if not is_valid_default_handler(handler): raise ConfigurationError( tag_("%(handler)s is not a valid default handler. Please " "update %(option)s through the %(page)s page or by " "directly editing trac.ini.", handler=tag.code(handler.__class__.__name__), option=tag.code("[trac] default_handler"), page=tag.a(_("Basic Settings"), href=req.href.admin('general/basics')))) return handler def _get_perm(self, req): if isinstance(req.session, FakeSession): return FakePerm() else: return PermissionCache(self.env, req.authname) def _get_session(self, req): try: return Session(self.env, req) except TracError as e: msg = "can't retrieve session: %s" if isinstance(e, TracValueError): self.log.warning(msg, e) else: self.log.error(msg, exception_to_unicode(e)) return FakeSession() def _get_locale(self, req): if has_babel: preferred = req.session.get('language') default = self.default_language negotiated = get_negotiated_locale([preferred, default] + req.languages) self.log.debug("Negotiated locale: %s -> %s", preferred, negotiated) return negotiated def _get_lc_time(self, req): lc_time = req.session.get('lc_time') if not lc_time or lc_time == 'locale' and not has_babel: lc_time = self.default_date_format if lc_time == 'iso8601': return 'iso8601' return req.locale def _get_timezone(self, req): try: return timezone(req.session.get('tz', self.default_timezone or 'missing')) except Exception: return localtz def _get_form_token(self, req): """Used to protect against CSRF. The 'form_token' is strong shared secret stored in a user cookie. By requiring that every POST form to contain this value we're able to protect against CSRF attacks. Since this value is only known by the user and not by an attacker. If the the user does not have a `trac_form_token` cookie a new one is generated. """ if 'trac_form_token' in req.incookie: return req.incookie['trac_form_token'].value else: req.outcookie['trac_form_token'] = form_token = hex_entropy(24) req.outcookie['trac_form_token']['path'] = req.base_path or '/' if self.env.secure_cookies: req.outcookie['trac_form_token']['secure'] = True req.outcookie['trac_form_token']['httponly'] = True return form_token def _get_use_xsendfile(self, req): return self.use_xsendfile @lazy def _xsendfile_header(self): header = self.xsendfile_header.strip() if Request.is_valid_header(header): return to_utf8(header) else: if not self._warn_xsendfile_header: self._warn_xsendfile_header = True self.log.warning("[trac] xsendfile_header is invalid: '%s'", header) return None def _get_xsendfile_header(self, req): return self._xsendfile_header @lazy def _configurable_headers(self): headers = [] invalids = [] for name, val in self.configurable_headers.options(): if Request.is_valid_header(name, val): headers.append((name, val)) else: invalids.append((name, val)) if invalids: self.log.warning('[http-headers] invalid headers are ignored: %s', ', '.join('%r: %r' % i for i in invalids)) return tuple(headers) def _get_configurable_headers(self, req): return iter(self._configurable_headers) def _pre_process_request(self, req, chosen_handler): for filter_ in self.filters: chosen_handler = filter_.pre_process_request(req, chosen_handler) return chosen_handler def _post_process_request(self, req, *args): metadata = {} resp = args if len(args) == 3: metadata = args[2] elif len(args) == 2: resp += (metadata,) elif len(args) == 0: resp = (None,) * 3 for f in reversed(self.filters): resp = f.post_process_request(req, *resp) if len(resp) == 2: resp += (metadata,) return resp
class Symbols(Component): """Replace character sequences with symbols. Characters and symbols are configurable in the `[wikiextras-symbols]` section in `trac.ini`. Use the `ShowSymbols` macro to display a list of currently defined symbols. """ implements(IWikiMacroProvider, IWikiSyntaxProvider) symbols_section = ConfigSection( 'wikiextras-symbols', """The set of symbols is configurable by providing associations between symbols and wiki keywords. A default set of symbols and keywords is defined, which can be revoked one-by-one (_remove) or all at once (_remove_defaults). Example: {{{ [wikiextras-symbols] _remove_defaults = true _remove = <- -> « = << » = >> ∑ = (SUM) ♥ = <3 }}} Keywords are space-separated! A symbol can also be removed by associating it with no keyword: {{{ ← = }}} Use the `ShowSymbols` macro to find out the current set of symbols and keywords. """) remove_defaults = BoolOption('wikiextras-symbols', '_remove_defaults', False, doc="Set to true to remove all " "default symbols.") remove = ListOption('wikiextras-symbols', '_remove', sep=' ', doc="""\ Space-separated(!) list of keywords that shall not be interpreted as symbols (even if defined in this section).""") def __init__(self): self.symbols = None # IWikiSyntaxProvider methods def get_wiki_syntax(self): if self.symbols is None: self.symbols = SYMBOLS.copy() if self.remove_defaults: self.symbols = {} for symbol, value in self.symbols_section.options(): if not symbol.startswith('_remove'): if value: for keyword in value.split(): self.symbols[keyword.strip()] = symbol else: # no keyword, remove all keywords associated with # symbol for k in self.symbols.keys(): if self.symbols[k] == symbol: del self.symbols[k] for keyword in self.remove: if keyword in self.symbols: del self.symbols[keyword] if self.symbols: yield ('!?%s' % prepare_regexp(self.symbols), self._format_symbol) else: yield (None, None) def get_link_resolvers(self): return [] #noinspection PyUnusedLocal def _format_symbol(self, formatter, match, fullmatch): return Markup(self.symbols[match]) # IWikiMacroProvider methods def get_macros(self): yield 'ShowSymbols' #noinspection PyUnusedLocal def get_macro_description(self, name): return ("Renders in a table the list of known symbols. " "Optional argument is the number of columns in the table " "(defaults 3).") #noinspection PyUnusedLocal def expand_macro(self, formatter, name, content, args=None): return render_table(self.symbols.keys(), content, lambda s: self._format_symbol(formatter, s, None), colspace=4)