class QueuesAjaxModule(Component): implements(IRequestHandler) audit = ChoiceOption( 'queues', 'audit', choices=['log', 'ticket', 'none'], doc="Record reorderings in log, in ticket, or not at all.") # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/queuesajax') def process_request(self, req): """Process AJAX request. Args come back in this form: id5=position1&id23=position2 where the name is the ticket id prefixed with 'id' and the value is the new position value prefixed with the name of the first column in the report. IMPORTANT: DO NOT RENAME THE FIRST COLUMN IN THE REPORT! This code assumes that the name of the first column in the report exactly matches the ticket's field name. This is to allow any position field name versus hard-coding it. """ try: changes = self._get_changes(req.args) self._save_changes(changes, req.authname) code, msg = 200, "OK" except Exception, e: import traceback code, msg = 500, "Oops...\n" + traceback.format_exc() + "\n" req.send_response(code) req.send_header('Content-Type', 'text/plain') req.send_header('Content-Length', len(msg)) req.end_headers() req.write(msg)
class TracWorkflowAdminModule(Component): implements(IAdminPanelProvider, ITemplateProvider, IEnvironmentSetupParticipant) operations = ListOption('workflow-admin', 'operations', 'del_owner, set_owner, set_owner_to_self, del_resolution, ' 'set_resolution, leave_status', doc=N_("Operations in workflow admin")) dot_path = Option('workflow-admin', 'dot_path', 'dot', doc=N_("Path to the dot executable")) diagram_cache = BoolOption('workflow-admin', 'diagram_cache', 'false', doc=N_("Enable cache of workflow diagram image")) diagram_size = Option('workflow-admin', 'diagram_size', '6, 6', doc=N_("Image size in workflow diagram")) diagram_font = Option('workflow-admin', 'diagram_font', 'sans-serif', doc=N_("Font name in workflow diagram")) diagram_fontsize = FloatOption('workflow-admin', 'diagram_fontsize', '10', doc=N_("Font size in workflow diagram")) diagram_colors = ListOption('workflow-admin', 'diagram_colors', '#0000ff, #006600, #ff0000, #666600, #ff00ff', doc=N_("Colors of arrows in workflow diagram")) default_editor = ChoiceOption( 'workflow-admin', 'default_editor', ['gui', 'text'], doc=N_("Default mode of the workflow editor")) auto_update_interval = Option( 'workflow-admin', 'auto_update_interval', '3000', doc=N_("An automatic-updating interval for text mode is specified by " "a milli second bit. It is not performed when 0 is specified.")) msgjs_locales = _msgjs_locales() _action_name_re = re.compile(r'\A[A-Za-z0-9_-]+\Z') _number_re = re.compile(r'\A[0-9]+\Z') def __init__(self): locale_dir = resource_filename(__name__, 'locale') add_domain(self.env.path, locale_dir) # IEnvironmentSetupParticipant def environment_created(self): pass def environment_needs_upgrade(self, db): return False def upgrade_environment(self, db): pass # ITemplateProvider method def get_htdocs_dirs(self): return [('tracworkflowadmin', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return [resource_filename(__name__, 'templates')] # IAdminPanelProvider methods def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('ticket', dgettext("messages", ("Ticket System")), 'workflowadmin', _("Workflow Admin")) def render_admin_panel(self, req, cat, page, path_info): req.perm.assert_permission('TRAC_ADMIN') if req.method == 'POST': self._parse_request(req) action, status = self._conf_to_inner_format(self.config) operations = self.operations permissions = self._get_permissions(req) add_stylesheet(req, 'tracworkflowadmin/themes/base/jquery-ui.css') add_stylesheet(req, 'tracworkflowadmin/css/tracworkflowadmin.css') add_stylesheet(req, 'tracworkflowadmin/css/jquery.multiselect.css') add_script(req, 'tracworkflowadmin/scripts/jquery-ui.js') add_script(req, 'tracworkflowadmin/scripts/jquery.json-2.2.js') add_script(req, 'tracworkflowadmin/scripts/jquery.multiselect.js') add_script(req, 'tracworkflowadmin/scripts/main.js') add_script_data(req, {'auto_update_interval': int(self.auto_update_interval)}) if req.locale and str(req.locale) in self.msgjs_locales: add_script(req, 'tracworkflowadmin/scripts/messages/%s.js' % req.locale) data = { 'actions': action, 'status': status, 'perms': permissions, 'operations': operations, 'editor_mode': req.args.get('editor_mode') or self.default_editor, 'text': self._conf_to_str(self.config) } return 'tracworkflowadmin.html', data def _conf_to_inner_format(self, conf): statuses = [] for name, value in conf.options('ticket-workflow'): if name.endswith('.operations') and 'leave_status' in [before.strip() for before in value.split(',')]: values = conf.get('ticket-workflow', name[0:-11]).split('->') if values[1].strip() == '*': for name in values[0].split(','): st = name.strip() if st != '*': statuses.append(st) break actions = {} count = 1 for name, value in conf.options('ticket-workflow'): param = name.split('.') actionName = param[0].strip() regValue = '' if len(param) == 1: pieces = [val.strip() for val in value.split('->')] before = pieces[0] next = '*' if len(pieces) > 1: next = pieces[1] regValue = {'next': next, 'before': {}} if next != '*' and next not in statuses: statuses.append(next) if before != '*': for val in before.split(','): tmp = val.strip() if tmp != '': regValue['before'][tmp] = 1 if tmp != '*' and tmp not in statuses: statuses.append(tmp) else: regValue['before'] = '*' if not actions.has_key(actionName): actions[actionName] = {'tempName': actionName, 'lineInfo': {}} actions[actionName]['next'] = regValue['next'] actions[actionName]['before'] = regValue['before'] else: regKey = param[1].strip() if regKey == 'permissions' or regKey == 'operations': tmp = [] for v in value.strip().split(','): tmp2 = v.strip() if tmp2 != '': tmp.append(v.strip()) regValue = tmp else: regValue = value.strip() if not actions.has_key(actionName): actions[actionName] = {'tempName': actionName, 'lineInfo': {}} actions[actionName][regKey] = regValue count = count + 1 action_elements = [] for key in actions: tmp = actions[key] tmp['action'] = key if not tmp.has_key('default'): tmp['default'] = 0 elif not self._number_re.match(tmp['default']): tmp['default'] = -1 if not tmp.has_key('permissions'): tmp['permissions'] = ['All Users'] if not tmp.has_key('name'): tmp['name'] = '' if tmp.has_key('before') and tmp['before'] == '*': tmp['before'] = {} for st in statuses: tmp['before'][st] = 1 action_elements.append(tmp) action_elements.sort(key=lambda v: int(v['default']), reverse=True) return (action_elements, statuses) def _conf_to_str(self, conf): tmp = ConfigParser.ConfigParser() tmp.add_section('ticket-workflow') for name, value in conf.options('ticket-workflow'): tmp.set('ticket-workflow', name.encode('utf-8'), value.encode('utf-8')) f = StringIO() tmp.write(f) f.flush() f.seek(0) lines = [line.decode('utf-8') for line in f if not line.startswith('[')] lines.sort() return ''.join(lines) def _str_to_inner_format(self, str, out): lines = str.splitlines(False) errors = [] lineInfo = {} firstLineInfo = {} # dict of (action, lineno) others = {} for idx, line in enumerate(lines): lineno = idx + 1 line = line.strip() lines[idx] = line if not line or line.startswith('#') or line.startswith(';'): continue if line.startswith('['): errors.append(_("Line %(num)d: Could not use section.", num=lineno)) continue if '=' not in line: errors.append(_( "Line %(num)d: This line is not pair of key and value.", num=lineno)) continue key, value = line.split('=', 1) key = key.strip().lower() value = value.strip() if key in lineInfo: errors.append(_( "Line %(num)d: There is a same key in line %(num2)d.", num=lineno, num2=lineInfo[key])) continue lineInfo[key] = lineno keys = key.split('.', 1) firstLineInfo.setdefault(keys[0], lineno) if len(keys) == 1: if '->' not in value: errors.append(_( "Line %(num)d: Must be \"<action> = <status-list> -> " "<new-status>\" format.", num=lineno)) continue stats = [stat.strip() for stat in value.split('->')[0].split(',')] for n, stat in enumerate(stats): if not stat: errors.append(_( "Line %(num)d: #%(n)d status is empty.", num=lineno, n=n + 1)) else: attr = keys[1] if '.' in attr: errors.append(_( "Line %(num)d: Must be \"<action>.<attribute> = " "<value>\" format.", num=lineno)) continue if attr not in ('default', 'name', 'operations', 'permissions'): others.setdefault(keys[0], {}) others[keys[0]][attr] = value if not firstLineInfo: errors.append(_("There is no valid description.")) actions = sorted(firstLineInfo.iterkeys()) for key in actions: if key not in lineInfo: errors.append(_( "Line %(num)d: Require \"%(action)s = <status-list> -> " "<new-status>\" line.", num=firstLineInfo[key], action=key)) if len(errors) != 0: out['textError'] = errors; return contents = '\n'.join(['[ticket-workflow]'] + lines).encode('utf-8') tmp_fd, tmp_file = mkstemp('.ini', 'workflow-admin') try: tmp_fp = os.fdopen(tmp_fd, 'w') tmp_fd = None try: tmp_fp.write(contents) finally: tmp_fp.close() tmp_conf = Configuration(tmp_file) finally: if tmp_fd is not None: os.close(tmp_fd) os.remove(tmp_file) try: out['actions'], out['status'] = self._conf_to_inner_format(tmp_conf) except ConfigParser.Error, e: out['textError'] = [to_unicode(e)] return out['lineInfo'] = lineInfo out['firstLineInfo'] = firstLineInfo out['others'] = others
class QueuesAjaxModule(Component): implements(IRequestHandler) audit = ChoiceOption( 'queues', 'audit', choices=['log', 'ticket', 'none'], doc="Record reorderings in log, in ticket, or not at all.") # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/queuesajax') def process_request(self, req): """Process AJAX request. Args come back in this form: id5=position1&id23=position2 where the name is the ticket id prefixed with 'id' and the value is the new position value prefixed with the name of the first column in the report. IMPORTANT: DO NOT RENAME THE FIRST COLUMN IN THE REPORT! This code assumes that the name of the first column in the report exactly matches the ticket's field name. This is to allow any position field name versus hard-coding it. """ try: changes = self._get_changes(req.args) self._save_changes(changes, req.authname) code, msg = 200, "OK" except Exception: import traceback code, msg = 500, "Oops...\n" + traceback.format_exc() + "\n" req.send_response(code) req.send_header('Content-Type', 'text/plain') req.send_header('Content-Length', len(msg)) req.end_headers() req.write(msg) # private methods def _get_changes(self, args): """Extract ticket ids and new position values from request args that are returned in this form: id5=position1&id23=position2 """ changes = {} keyval_re = re.compile(r"(?P<key>[^0-9]+)(?P<val>[0-9]*)") for key, val in args.items(): # get ticket id match = keyval_re.search(key) if not match: continue id = match.groupdict()['val'] if not id: continue # get position field name and value match = keyval_re.search(val) if not match: continue field = match.groupdict()['key'] new_pos = match.groupdict().get('val', '') changes[id] = (field, new_pos) return changes def _save_changes(self, changes, author): """Save ticket changes.""" if self.audit in ('log', 'none'): with self.env.db_transaction as db: cursor = db.cursor() for id, (field, new_pos) in changes.items(): cursor.execute( """ SELECT value from ticket_custom WHERE name=%s AND ticket=%s """, (field, id)) result = cursor.fetchone() if result: old_pos = result[0] cursor.execute( """ UPDATE ticket_custom SET value=%s WHERE name=%s AND ticket=%s """, (new_pos, field, id)) else: old_pos = '(none)' cursor.execute( """ INSERT INTO ticket_custom (ticket,name,value) VALUES (%s,%s,%s) """, (id, field, new_pos)) if self.audit == 'log': self.log.info( "%s reordered ticket #%s's %s from %s to %s" % (author, id, field, old_pos, new_pos)) else: for id, (field, new_pos) in changes.items(): ticket = Ticket(self.env, id) ticket[field] = new_pos ticket.save_changes(author=author, comment='')
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 SubversionConnector(Component): implements(ISystemInfoProvider, IRepositoryConnector) required = False branches = ListOption('svn', 'branches', 'trunk, branches/*', doc= """Comma separated list of paths categorized as branches. If a path ends with '*', then all the directory entries found below that path will be included. Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*` """) tags = ListOption('svn', 'tags', 'tags/*', doc= """Comma separated list of paths categorized as tags. If a path ends with '*', then all the directory entries found below that path will be included. Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1` """) eol_style = ChoiceOption( 'svn', 'eol_style', ['native', 'LF', 'CRLF', 'CR'], doc= """End-of-Line character sequences when `svn:eol-style` property is `native`. If `native`, substitute with the native EOL marker on the server. Otherwise, if `LF`, `CRLF` or `CR`, substitute with the specified EOL marker. (''since 1.0.2'')""") error = None def __init__(self): self._version = None try: _import_svn() self.log.debug('Subversion bindings imported') except ImportError as e: self.error = e self.log.info('Failed to load Subversion bindings', exc_info=True) else: version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) self._version = '%d.%d.%d' % version + core.SVN_VER_TAG if version[0] < 1: self.error = _("Subversion >= 1.0 required, found %(version)s", version=self._version) Pool() # ISystemInfoProvider methods def get_system_info(self): if self.required: yield 'Subversion', self._version # IRepositoryConnector methods def get_supported_types(self): prio = 1 if self.error: prio = -1 yield ("direct-svnfs", prio * 4) yield ("svnfs", prio * 4) yield ("svn", prio * 2) def get_repository(self, type, dir, params): """Return a `SubversionRepository`. The repository is wrapped in a `CachedRepository`, unless `type` is 'direct-svnfs'. """ params.update(tags=self.tags, branches=self.branches) params.setdefault('eol_style', self.eol_style) repos = SubversionRepository(dir, params, self.log) if type != 'direct-svnfs': repos = SvnCachedRepository(self.env, repos, self.log) self.required = True return repos
class MasterTicketsModule(Component): """Provides support for ticket dependencies.""" implements(IRequestFilter, IRequestHandler, ITemplateProvider, ITemplateStreamFilter) dot_path = Option('mastertickets', 'dot_path', default='dot', doc="Path to the dot executable.") gs_path = Option('mastertickets', 'gs_path', default='gs', doc="Path to the ghostscript executable.") use_gs = BoolOption( 'mastertickets', 'use_gs', default=False, doc="If enabled, use ghostscript to produce nicer output.") acceptable_formats = ListOption( 'mastertickets', 'acceptable_formats', default='png,cmapx', sep=',', doc="""The formats that may be chosen; execute dot -T? for a list of options.""") closed_color = Option('mastertickets', 'closed_color', default='green', doc="Color of closed tickets") opened_color = Option('mastertickets', 'opened_color', default='red', doc="Color of opened tickets") show_key = Option('mastertickets', 'show_key', default=False, doc="Show a key for open/closed nodes") closed_text = Option('mastertickets', 'closed_text', default='Done', doc="Text for key showing closed tickets") opened_text = Option('mastertickets', 'opened_text', default='ToDo', doc="Text for key showing opened tickets") highlight_target = Option('mastertickets', 'highlight_target', default=False, doc="Highlight target tickets in graph") full_graph = Option( 'mastertickets', 'full_graph', default=False, doc="Show full dep. graph, not just direct blocking links") graph_direction = ChoiceOption( 'mastertickets', 'graph_direction', choices=['TD', 'LR', 'DT', 'RL'], doc="""Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left).""") fields = set(['blocking', 'blockedby']) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if template is not None: if req.path_info.startswith('/ticket/'): # In case of an invalid ticket, the data is invalid if not data: return template, data, content_type tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/js/disable_resolve.js') break # Add link to depgraph if needed. if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph('ticket', tkt.id)) for change in data.get('changes', {}): if 'fields' not in change: continue for field, field_data in change['fields'].iteritems(): if field in self.fields: # Skip if rendered already if 'rendered' in field_data: continue if field_data['new'].strip(): new = to_int_set(field_data['new']) else: new = set() if field_data['old'].strip(): old = to_int_set(field_data['old']) else: old = set() add = new - old sub = old - new elms = html() if add: elms.append( html.em(u', '.join( unicode(n) for n in sorted(add)))) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( html.em(u', '.join( unicode(n) for n in sorted(sub)))) elms.append(u' removed') field_data['rendered'] = elms # Add a link to generate a dependency graph for all the tickets # in the milestone if req.path_info.startswith('/milestone/'): if not data: return template, data, content_type milestone = data['milestone'] add_ctxtnav(req, 'Depgraph', req.href.depgraph('milestone', milestone.name)) return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if not data: return stream # Try all at the same time to catch changed or processed templates. if filename in [ 'report_view.html', 'query_results.html', 'ticket.html', 'query.html' ]: # For ticket.html if 'fields' in data and isinstance(data['fields'], list): for field in data['fields']: for f in self.fields: if field['name'] == f and data['ticket'][f]: field['rendered'] = \ self._link_tickets(req, data['ticket'][f]) # For query_results.html and query.html if 'groups' in data and isinstance(data['groups'], list): for group, tickets in data['groups']: for ticket in tickets: for f in self.fields: if f in ticket: ticket[f] = self._link_tickets(req, ticket[f]) # For report_view.html if 'row_groups' in data and isinstance(data['row_groups'], list): for group, rows in data['row_groups']: for row in rows: if 'cell_groups' in row and \ isinstance(row['cell_groups'], list): for cells in row['cell_groups']: for cell in cells: # If the user names column in the report # differently (blockedby AS "blocked by") # then this will not find it if cell.get('header', {}).get('col') \ in self.fields: cell['value'] = \ self._link_tickets(req, cell['value']) return stream # ITemplateProvider methods def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('mastertickets', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] # IRequestHandler methods def match_request(self, req): match = re.match( r'^/depgraph/(?P<realm>ticket|milestone)/' r'(?P<id>((?!depgraph.png).)+)(/depgraph.png)?$', req.path_info) if match: req.args['realm'] = match.group('realm') req.args['id'] = match.group('id') return True def process_request(self, req): realm = req.args['realm'] id_ = req.args['id'] if not which(self.dot_path): raise TracError( _("Path to dot executable is invalid: %(path)s", path=self.dot_path)) # Urls to generate the depgraph for a ticket is /depgraph/ticketnum # Urls to generate the depgraph for a milestone is # /depgraph/milestone/milestone_name # List of tickets to generate the depgraph. if realm == 'milestone': # We need to query the list of tickets in the milestone query = Query(self.env, constraints={'milestone': [id_]}, max=0) tkt_ids = [fields['id'] for fields in query.execute(req)] else: tid = as_int(id_, None) if tid is None: raise TracError( tag_("%(id)s is not a valid ticket id.", id=html.tt(id_))) tkt_ids = [tid] # The summary argument defines whether we place the ticket id or # its summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if req.path_info.endswith('/depgraph.png') or 'format' in req.args: format_ = req.args.get('format') if format_ == 'text': # In case g.__str__ returns unicode, convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format_ == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format_ is not None: if format_ in self.acceptable_formats: req.send(g.render(self.dot_path, format_), 'text/plain') else: raise TracError( _("The %(format)s format is not allowed.", format=format_)) if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} # Add a context link to enable/disable labels in nodes. if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if realm == 'milestone': add_ctxtnav(req, 'Back to Milestone: %s' % id_, req.href.milestone(id_)) data['milestone'] = id_ else: data['ticket'] = id_ add_ctxtnav(req, 'Back to Ticket #%s' % id_, req.href.ticket(id_)) data['graph'] = g data['graph_render'] = functools.partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None def _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph(log=self.log) g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for tid in tkt_ids: g[tid] if self.show_key: g[-1]['label'] = self.closed_text g[-1]['fillcolor'] = self.closed_color g[-1]['shape'] = 'box' g[-2]['label'] = self.opened_text g[-2]['fillcolor'] = self.opened_color g[-2]['shape'] = 'box' links = TicketLinks.walk_tickets(self.env, tkt_ids, self.full_graph) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt['status'] == 'closed' and \ self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = escape(tkt['summary']) if self.highlight_target and tkt.id in tkt_ids: node['penwidth'] = 3 for n in link.blocking: node > g[n] return g def _link_tickets(self, req, tickets): items = [] for i, word in enumerate(re.split(r'([;,\s]+)', tickets)): if i % 2: items.append(word) elif word: tid = word word = '#%s' % word try: ticket = Ticket(self.env, tid) if 'TICKET_VIEW' in req.perm(ticket.resource): word = \ html.a( '#%s' % ticket.id, href=req.href.ticket(ticket.id), class_=classes(ticket['status'], 'ticket'), title=get_resource_summary(self.env, ticket.resource) ) except ResourceNotFound: pass items.append(word) if items: return html(items) else: return None
class SubTicketsModule(Component): implements(IRequestFilter, ITicketManipulator, ITemplateProvider, ITemplateStreamFilter) # Simple Options opt_skip_validation = ListOption('subtickets', 'skip_closure_validation', default=[], doc=_(""" Normally, reopening a child with a `closed` parent will be refused and closing a parent with non-`closed` children will also be refused. Adding either of `reopen` or `resolve` to this option will make Subtickets skip this validation for the respective action. Separate by comma if both actions are listed. Caveat: This functionality will be made workflow-independent in a future release of !SubTicketsPlugin. """)) opt_recursion_depth = IntOption('subtickets', 'recursion_depth', default=-1, doc=_(""" Limit the number of recursive levels when listing subtickets. Default is infinity, represented by`-1`. The value zero (0) limits the listing to immediate children. """)) opt_add_style = ChoiceOption('subtickets', 'add_style', ['button', 'link'], doc=_(""" Choose whether to make `Add` look like a button (default) or a link """)) opt_owner_url = Option('subtickets', 'owner_url', doc=_(""" Currently undocumented. """)) # Per-ticket type options -- all initialised in __init__() opt_inherit_fields = dict() opt_columns = dict() def _add_per_ticket_type_option(self, ticket_type): self.opt_inherit_fields[ticket_type] = ListOption( 'subtickets', 'type.%s.child_inherits' % ticket_type, default='', doc=_("""Comma-separated list of ticket fields whose values are to be copied from a parent ticket into a newly created child ticket. """)) self.opt_columns[ticket_type] = ListOption('subtickets', 'type.%s.table_columns' % ticket_type, default='status,owner', doc=_(""" Comma-separated list of ticket fields whose values are to be shown for each child ticket in the subtickets list """)) def __init__(self): # The following initialisations must happen inside init() # in order to be able to access self.env for tt in TicketType.select(self.env): self._add_per_ticket_type_option(tt.name) # ITemplateProvider methods def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('subtickets', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): return [] # 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'): # get parent ticket's data if data and 'ticket' in data: ticket = data['ticket'] parents = ticket['parents'] or '' ids = set(NUMBERS_RE.findall(parents)) if len(parents) > 0: self._append_parent_links(req, data, ids) children = self.get_children(ticket.id) if children: data['subtickets'] = children elif path.startswith('/admin/ticket/type') \ and data \ and set(['add', 'name']).issubset(data.keys()) \ and data['add'] == 'Add': self._add_per_ticket_type_option(data['name']) return template, data, content_type def _append_parent_links(self, req, data, ids): links = [] for id in sorted(ids, key=lambda x: int(x)): try: ticket = Ticket(self.env, id) elem = tag.a('#%s' % id, href=req.href.ticket(id), class_='%s ticket' % ticket['status'], title=ticket['summary']) if len(links) > 0: links.append(', ') links.append(elem) except ResourceNotFound: pass for field in data.get('fields', ''): if field.get('name') == 'parents': field['rendered'] = tag.span(*links) # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def get_children(self, parent_id, depth=0): children = {} for parent, child in self.env.db_query( """ SELECT parent, child FROM subtickets WHERE parent=%s """, (parent_id, )): children[child] = None if self.opt_recursion_depth > depth or self.opt_recursion_depth == -1: for id in children: children[id] = self.get_children(id, depth + 1) return children def validate_ticket(self, req, ticket): action = req.args.get('action') if action in self.opt_skip_validation: return if action == 'resolve': for parent, child in self.env.db_query( """ SELECT parent, child FROM subtickets WHERE parent=%s """, (ticket.id, )): if Ticket(self.env, child)['status'] != 'closed': yield None, _("""Cannot close/resolve because child ticket #%(child)s is still open""", child=child) elif action == 'reopen': ids = set(NUMBERS_RE.findall(ticket['parents'] or '')) for id in ids: if Ticket(self.env, id)['status'] == 'closed': msg = _( "Cannot reopen because parent ticket #%(id)s " "is closed", id=id) yield None, msg # ITemplateStreamFilter method def _create_subtickets_table(self, req, children, tbody, depth=0): """Recursively create list table of subtickets """ if not children: return for id in sorted(children, key=lambda x: int(x)): ticket = Ticket(self.env, id) # the row r = [] # Always show ID and summary attrs = {'href': req.href.ticket(id)} if ticket['status'] == 'closed': attrs['class_'] = 'closed' link = tag.a('#%s' % id, **attrs) summary = tag.td(link, ': %s' % ticket['summary'], style='padding-left: %dpx;' % (depth * 15)) r.append(summary) # Add other columns as configured. for column in \ self.env.config.getlist('subtickets', 'type.%(type)s.table_columns' % ticket): if column == 'owner': if self.opt_owner_url: href = req.href(self.opt_owner_url % ticket['owner']) else: href = req.href.query(status='!closed', owner=ticket['owner']) e = tag.td(tag.a(ticket['owner'], href=href)) elif column == 'milestone': href = req.href.query(status='!closed', milestone=ticket['milestone']) e = tag.td(tag.a(ticket['milestone'], href=href)) else: e = tag.td(ticket[column]) r.append(e) tbody.append(tag.tr(*r)) self._create_subtickets_table(req, children[id], tbody, depth + 1) def filter_stream(self, req, method, filename, stream, data): if not req.path_info.startswith('/ticket/'): return stream div = None link = None button = None if 'ticket' in data: # get parents data ticket = data['ticket'] # title div = tag.div(class_='description') if 'TICKET_CREATE' in req.perm(ticket.resource) \ and ticket['status'] != 'closed': opt_inherit = self.env.config.getlist( 'subtickets', 'type.%(type)s.child_inherits' % ticket) if self.opt_add_style == 'link': inh = {f: ticket[f] for f in opt_inherit} link = tag.a(_('add'), href=req.href.newticket(parents=ticket.id, **inh)) link = tag.span('(', link, ')', class_='addsubticket') else: inh = [ tag.input(type='hidden', name=f, value=ticket[f]) for f in opt_inherit ] button = tag.form(tag.div(tag.input( type="submit", value=_("Create"), title=_("Create a child ticket")), inh, tag.input(type="hidden", name="parents", value=str(ticket.id)), class_="inlinebuttons"), method="get", action=req.href.newticket()) div.append(button) div.append(tag.h3(_('Subtickets '), link)) if 'subtickets' in data: # table tbody = tag.tbody() div.append(tag.table(tbody, class_='subtickets')) # tickets self._create_subtickets_table(req, data['subtickets'], tbody) if div: add_stylesheet(req, 'subtickets/css/subtickets.css') ''' If rendered in preview mode, DIV we're interested in isn't a child but the root and transformation won't succeed. According to HTML specification, id's must be unique within a document, so it's safe to omit the leading '.' in XPath expression to select all matching regardless of hierarchy their in. ''' stream |= Transformer('//div[@id="ticket"]').append(div) return stream
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
class MasterTicketsModule(Component): """Provides support for ticket dependencies.""" implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter, ITemplateProvider, ITicketManipulator) dot_path = Option('mastertickets', 'dot_path', default='dot', doc='Path to the dot executable.') gs_path = Option('mastertickets', 'gs_path', default='gs', doc='Path to the ghostscript executable.') use_gs = BoolOption( 'mastertickets', 'use_gs', default=False, doc='If enabled, use ghostscript to produce nicer output.') closed_color = Option('mastertickets', 'closed_color', default='green', doc='Color of closed tickets') opened_color = Option('mastertickets', 'opened_color', default='red', doc='Color of opened tickets') graph_direction = ChoiceOption( 'mastertickets', 'graph_direction', choices=['TD', 'LR', 'DT', 'RL'], doc= 'Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)' ) FIELD_XPATH = '//div[@id="ticket"]/table[@class="properties"]//td[@headers="h_%s"]/text()' fields = set(['blocking', 'blockedby']) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if req.path_info.startswith('/ticket/'): # In case of an invalid ticket, the data is invalid if not data: return template, data, content_type tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/disable_resolve.js') break data['mastertickets'] = { 'field_values': { 'blocking': linkify_ids(self.env, req, links.blocking), 'blockedby': linkify_ids(self.env, req, links.blocked_by), }, } # Add link to depgraph if needed if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id)) for change in data.get('changes', {}): if not change.has_key('fields'): continue for field, field_data in change['fields'].iteritems(): if field in self.fields: if field_data['new'].strip(): new = set( [int(n) for n in field_data['new'].split(',')]) else: new = set() if field_data['old'].strip(): old = set( [int(n) for n in field_data['old'].split(',')]) else: old = set() add = new - old sub = old - new elms = tag() if add: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(add)]))) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(sub)]))) elms.append(u' removed') field_data['rendered'] = elms #add a link to generate a dependency graph for all the tickets in the milestone if req.path_info.startswith('/milestone/'): if not data: return template, data, content_type milestone = data['milestone'] add_ctxtnav(req, 'Depgraph', req.href.depgraph('milestone', milestone.name)) return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if 'mastertickets' in data: for field, value in data['mastertickets'][ 'field_values'].iteritems(): stream |= Transformer(self.FIELD_XPATH % field).replace(value) return stream # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): if req.args.get('action') == 'resolve' and req.args.get( 'action_resolve_resolve_resolution') == 'fixed': links = TicketLinks(self.env, ticket) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': yield None, 'Ticket #%s is blocking this ticket' % i # ITemplateProvider methods def get_htdocs_dirs(self): """Return the absolute path of a directory containing additional static resources (such as images, style sheets, etc). """ return [('mastertickets', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): """Return the absolute path of the directory containing the provided ClearSilver templates. """ return [resource_filename(__name__, 'templates')] # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/depgraph') def process_request(self, req): path_info = req.path_info[10:] if not path_info: raise TracError('No ticket specified') #list of tickets to generate the depgraph for tkt_ids = [] milestone = None split_path = path_info.split('/', 2) #Urls to generate the depgraph for a ticket is /depgraph/ticketnum #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name if split_path[0] == 'milestone': #we need to query the list of tickets in the milestone milestone = split_path[1] query = Query(self.env, constraints={'milestone': [milestone]}, max=0) tkt_ids = [fields['id'] for fields in query.execute()] else: #the list is a single ticket tkt_ids = [int(split_path[0])] #the summary argument defines whether we place the ticket id or #it's summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if path_info.endswith('/depgraph.png') or 'format' in req.args: format = req.args.get('format') if format == 'text': #in case g.__str__ returns unicode, we need to convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format is not None: req.send(g.render(self.dot_path, format), 'text/plain') if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} #add a context link to enable/disable labels in nodes if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if milestone is None: tkt = Ticket(self.env, tkt_ids[0]) data['tkt'] = tkt add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id, req.href.ticket(tkt.id)) else: add_ctxtnav(req, 'Back to Milestone %s' % milestone, req.href.milestone(milestone)) data['milestone'] = milestone data['graph'] = g data['graph_render'] = partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None def _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph() g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] links = TicketLinks.walk_tickets(self.env, tkt_ids) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt[ 'status'] == 'closed' and self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = tkt['summary'] for n in link.blocking: node > g[n] return g
class EmailDistributor(Component): implements(IAnnouncementDistributor, IAnnouncementPreferenceProvider) formatters = ExtensionPoint(IAnnouncementFormatter) producers = ExtensionPoint(IAnnouncementProducer) distributors = ExtensionPoint(IAnnouncementDistributor) # Make ordered decorators = ExtensionPoint(IAnnouncementEmailDecorator) resolvers = OrderedExtensionsOption('announcer', 'email_address_resolvers', IAnnouncementAddressResolver, 'SpecifiedEmailResolver, '\ 'SessionEmailResolver, DefaultDomainEmailResolver', """Comma seperated list of email resolver components in the order they will be called. If an email address is resolved, the remaining resolvers will not be called. """) email_sender = ExtensionOption( 'announcer', 'email_sender', IEmailSender, 'SmtpEmailSender', """Name of the component implementing `IEmailSender`. This component is used by the announcer system to send emails. Currently, `SmtpEmailSender` and `SendmailEmailSender` are provided. """) enabled = BoolOption('announcer', 'email_enabled', 'true', """Enable email notification.""") email_from = Option('announcer', 'email_from', 'trac@localhost', """Sender address to use in notification emails.""") from_name = Option('announcer', 'email_from_name', '', """Sender name to use in notification emails.""") replyto = Option('announcer', 'email_replyto', 'trac@localhost', """Reply-To address to use in notification emails.""") mime_encoding = ChoiceOption( 'announcer', 'mime_encoding', ['base64', 'qp', 'none'], """Specifies the MIME encoding scheme for emails. Valid options are 'base64' for Base64 encoding, 'qp' for Quoted-Printable, and 'none' for no encoding. Note that the no encoding means that non-ASCII characters in text are going to cause problems with notifications. """) use_public_cc = BoolOption( 'announcer', 'use_public_cc', 'false', """Recipients can see email addresses of other CC'ed recipients. If this option is disabled (the default), recipients are put on BCC """) # used in email decorators, but not here subject_prefix = Option( 'announcer', 'email_subject_prefix', '__default__', """Text to prepend to subject line of notification emails. If the setting is not defined, then the [$project_name] prefix. If no prefix is desired, then specifying an empty option will disable it. """) to = Option('announcer', 'email_to', 'undisclosed-recipients: ;', 'Default To: field') use_threaded_delivery = BoolOption( 'announcer', 'use_threaded_delivery', 'false', """Do message delivery in a separate thread. Enabling this will improve responsiveness for requests that end up with an announcement being sent over email. It requires building Python with threading support enabled-- which is usually the case. To test, start Python and type 'import threading' to see if it raises an error. """) default_email_format = Option( 'announcer', 'default_email_format', 'text/plain', """The default mime type of the email notifications. This can be overridden on a per user basis through the announcer preferences panel. """) set_message_id = BoolOption( 'announcer', 'set_message_id', 'true', """Disable if you would prefer to let the email server handle message-id generation. """) rcpt_allow_regexp = Option( 'announcer', 'rcpt_allow_regexp', '', """A whitelist pattern to match any address to before adding to recipients list. """) rcpt_local_regexp = Option( 'announcer', 'rcpt_local_regexp', '', """A whitelist pattern to match any address, that should be considered local. This will be evaluated only if msg encryption is set too. Recipients with matching email addresses will continue to receive unencrypted email messages. """) crypto = Option( 'announcer', 'email_crypto', '', """Enable cryptographically operation on email msg body. Empty string, the default for unset, disables all crypto operations. Valid values are: sign sign msg body with given privkey encrypt encrypt msg body with pubkeys of all recipients sign,encrypt sign, than encrypt msg body """) # get GnuPG configuration options gpg_binary = Option( 'announcer', 'gpg_binary', 'gpg', """GnuPG binary name, allows for full path too. Value 'gpg' is same default as in python-gnupg itself. For usual installations location of the gpg binary is auto-detected. """) gpg_home = Option( 'announcer', 'gpg_home', '', """Directory containing keyring files. In case of wrong configuration missing keyring files without content will be created in the configured location, provided necessary write permssion is granted for the corresponding parent directory. """) private_key = Option( 'announcer', 'gpg_signing_key', None, """Keyid of private key (last 8 chars or more) used for signing. If unset, a private key will be selected from keyring automagicly. The password must be available i.e. provided by running gpg-agent or empty (bad security). On failing to unlock the private key, msg body will get emptied. """) def __init__(self): self.delivery_queue = None self._init_pref_encoding() def get_delivery_queue(self): if not self.delivery_queue: self.delivery_queue = Queue.Queue() thread = DeliveryThread(self.delivery_queue, self.send) thread.start() return self.delivery_queue # IAnnouncementDistributor def transports(self): yield "email" def formats(self, transport, realm): "Find valid formats for transport and realm" formats = {} for f in self.formatters: for style in f.styles(transport, realm): formats[style] = f self.log.debug( "EmailDistributor has found the following formats capable " "of handling '%s' of '%s': %s" % (transport, realm, ', '.join(formats.keys()))) if not formats: self.log.error("EmailDistributor is unable to continue " \ "without supporting formatters.") return formats def distribute(self, transport, recipients, event): found = False for supported_transport in self.transports(): if supported_transport == transport: found = True if not self.enabled or not found: self.log.debug("EmailDistributer email_enabled set to false") return fmtdict = self.formats(transport, event.realm) if not fmtdict: self.log.error("EmailDistributer No formats found for %s %s" % (transport, event.realm)) return msgdict = {} msgdict_encrypt = {} msg_pubkey_ids = [] # compile pattern before use for better performance RCPT_ALLOW_RE = re.compile(self.rcpt_allow_regexp) RCPT_LOCAL_RE = re.compile(self.rcpt_local_regexp) if self.crypto != '': self.log.debug("EmailDistributor attempts crypto operation.") self.enigma = CryptoTxt(self.gpg_binary, self.gpg_home) for name, authed, addr in recipients: fmt = name and \ self._get_preferred_format(event.realm, name, authed) or \ self._get_default_format() if fmt not in fmtdict: self.log.debug(("EmailDistributer format %s not available " + "for %s %s, looking for an alternative") % (fmt, transport, event.realm)) # If the fmt is not available for this realm, then try to find # an alternative oldfmt = fmt fmt = None for f in fmtdict.values(): fmt = f.alternative_style_for(transport, event.realm, oldfmt) if fmt: break if not fmt: self.log.error( "EmailDistributer was unable to find a formatter " + "for format %s" % k) continue rslvr = None if name and not addr: # figure out what the addr should be if it's not defined for rslvr in self.resolvers: addr = rslvr.get_address_for_name(name, authed) if addr: break if addr: self.log.debug("EmailDistributor found the " \ "address '%s' for '%s (%s)' via: %s"%( addr, name, authed and \ 'authenticated' or 'not authenticated', rslvr.__class__.__name__)) # ok, we found an addr, add the message # but wait, check for allowed rcpt first, if set if RCPT_ALLOW_RE.search(addr) is not None: # check for local recipients now local_match = RCPT_LOCAL_RE.search(addr) if self.crypto in ['encrypt', 'sign,encrypt'] and \ local_match is None: # search available public keys for matching UID pubkey_ids = self.enigma.get_pubkey_ids(addr) if len(pubkey_ids) > 0: msgdict_encrypt.setdefault(fmt, set()).add( (name, authed, addr)) msg_pubkey_ids[len(msg_pubkey_ids):] = pubkey_ids self.log.debug("EmailDistributor got pubkeys " \ "for %s: %s" % (addr, pubkey_ids)) else: self.log.debug("EmailDistributor dropped %s " \ "after missing pubkey with corresponding " \ "address %s in any UID" % (name, addr)) else: msgdict.setdefault(fmt, set()).add( (name, authed, addr)) if local_match is not None: self.log.debug("EmailDistributor expected " \ "local delivery for %s to: %s" % (name, addr)) else: self.log.debug("EmailDistributor dropped %s for " \ "not matching allowed recipient pattern %s" % \ (addr, self.rcpt_allow_regexp)) else: self.log.debug("EmailDistributor was unable to find an " \ "address for: %s (%s)"%(name, authed and \ 'authenticated' or 'not authenticated')) for k, v in msgdict.items(): if not v or not fmtdict.get(k): continue self.log.debug("EmailDistributor is sending event as '%s' to: %s" % (fmt, ', '.join(x[2] for x in v))) self._do_send(transport, event, k, v, fmtdict[k]) for k, v in msgdict_encrypt.items(): if not v or not fmtdict.get(k): continue self.log.debug( "EmailDistributor is sending encrypted info on event " \ "as '%s' to: %s"%(fmt, ', '.join(x[2] for x in v))) self._do_send(transport, event, k, v, fmtdict[k], msg_pubkey_ids) def _get_default_format(self): return self.default_email_format def _get_preferred_format(self, realm, sid, authenticated): if authenticated is None: authenticated = 0 db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute( """ SELECT value FROM session_attribute WHERE sid=%s AND authenticated=%s AND name=%s """, (sid, int(authenticated), 'announcer_email_format_%s' % realm)) result = cursor.fetchone() if result: chosen = result[0] self.log.debug("EmailDistributor determined the preferred format" \ " for '%s (%s)' is: %s"%(sid, authenticated and \ 'authenticated' or 'not authenticated', chosen)) return chosen else: return self._get_default_format() def _init_pref_encoding(self): self._charset = Charset() self._charset.input_charset = 'utf-8' pref = self.mime_encoding.lower() if pref == 'base64': self._charset.header_encoding = BASE64 self._charset.body_encoding = BASE64 self._charset.output_charset = 'utf-8' self._charset.input_codec = 'utf-8' self._charset.output_codec = 'utf-8' elif pref in ['qp', 'quoted-printable']: self._charset.header_encoding = QP self._charset.body_encoding = QP self._charset.output_charset = 'utf-8' self._charset.input_codec = 'utf-8' self._charset.output_codec = 'utf-8' elif pref == 'none': self._charset.header_encoding = None self._charset.body_encoding = None self._charset.input_codec = None self._charset.output_charset = 'ascii' else: raise TracError(_('Invalid email encoding setting: %s' % pref)) def _message_id(self, realm): """Generate an unique message ID.""" modtime = time.time() s = '%s.%d.%s' % (self.env.project_url, modtime, realm.encode('ascii', 'ignore')) dig = md5(s).hexdigest() host = self.email_from[self.email_from.find('@') + 1:] msgid = '<%03d.%s@%s>' % (len(s), dig, host) return msgid def _filter_recipients(self, rcpt): return rcpt def _do_send(self, transport, event, format, recipients, formatter, pubkey_ids=[]): output = formatter.format(transport, event.realm, format, event) # DEVEL: force message body plaintext style for crypto operations if self.crypto != '' and pubkey_ids != []: if self.crypto == 'sign': output = self.enigma.sign(output, self.private_key) elif self.crypto == 'encrypt': output = self.enigma.encrypt(output, pubkey_ids) elif self.crypto == 'sign,encrypt': output = self.enigma.sign_encrypt(output, pubkey_ids, self.private_key) self.log.debug(output) self.log.debug(_("EmailDistributor crypto operaton successful.")) alternate_output = None else: alternate_style = formatter.alternative_style_for( transport, event.realm, format) if alternate_style: alternate_output = formatter.format(transport, event.realm, alternate_style, event) else: alternate_output = None # sanity check if not self._charset.body_encoding: try: dummy = output.encode('ascii') except UnicodeDecodeError: raise TracError(_("Ticket contains non-ASCII chars. " \ "Please change encoding setting")) rootMessage = MIMEMultipart("related") headers = dict() if self.set_message_id: # A different, predictable, but still sufficiently unique # message ID will be generated as replacement in # announcer.email_decorators.generic.ThreadingEmailDecorator # for email threads to work. headers['Message-ID'] = self._message_id(event.realm) headers['Date'] = formatdate() from_header = formataddr((self.from_name or self.env.project_name, self.email_from)) headers['From'] = from_header headers['To'] = '"%s"' % (self.to) if self.use_public_cc: headers['Cc'] = ', '.join([x[2] for x in recipients if x]) headers['Reply-To'] = self.replyto for k, v in headers.iteritems(): set_header(rootMessage, k, v) rootMessage.preamble = 'This is a multi-part message in MIME format.' if alternate_output: parentMessage = MIMEMultipart('alternative') rootMessage.attach(parentMessage) alt_msg_format = 'html' in alternate_style and 'html' or 'plain' msgText = MIMEText(alternate_output, alt_msg_format) parentMessage.attach(msgText) else: parentMessage = rootMessage msg_format = 'html' in format and 'html' or 'plain' msgText = MIMEText(output, msg_format) del msgText['Content-Transfer-Encoding'] msgText.set_charset(self._charset) parentMessage.attach(msgText) decorators = self._get_decorators() if len(decorators) > 0: decorator = decorators.pop() decorator.decorate_message(event, rootMessage, decorators) recip_adds = [x[2] for x in recipients if x] # Append any to, cc or bccs added to the recipient list for field in ('To', 'Cc', 'Bcc'): if rootMessage[field] and \ len(str(rootMessage[field]).split(',')) > 0: for addy in str(rootMessage[field]).split(','): self._add_recipient(recip_adds, addy) # replace with localized bcc hint if headers['To'] == 'undisclosed-recipients: ;': set_header(rootMessage, 'To', _('undisclosed-recipients: ;')) self.log.debug("Content of recip_adds: %s" % (recip_adds)) package = (from_header, recip_adds, rootMessage.as_string()) start = time.time() if self.use_threaded_delivery: self.get_delivery_queue().put(package) else: self.send(*package) stop = time.time() self.log.debug("EmailDistributor took %s seconds to send."\ %(round(stop-start,2))) def send(self, from_addr, recipients, message): """Send message to recipients via e-mail.""" # Ensure the message complies with RFC2822: use CRLF line endings message = CRLF.join(re.split("\r?\n", message)) self.email_sender.send(from_addr, recipients, message) def _get_decorators(self): return self.decorators[:] def _add_recipient(self, recipients, addy): if addy.strip() != '"undisclosed-recipients: ;"': recipients.append(addy) # IAnnouncementDistributor def get_announcement_preference_boxes(self, req): yield "email", _("E-Mail Format") def render_announcement_preference_box(self, req, panel): supported_realms = {} for producer in self.producers: for realm in producer.realms(): for distributor in self.distributors: for transport in distributor.transports(): for fmtr in self.formatters: for style in fmtr.styles(transport, realm): if realm not in supported_realms: supported_realms[realm] = set() supported_realms[realm].add(style) if req.method == "POST": for realm in supported_realms: opt = req.args.get('email_format_%s' % realm, False) if opt: req.session['announcer_email_format_%s' % realm] = opt prefs = {} for realm in supported_realms: prefs[realm] = req.session.get('announcer_email_format_%s' % realm, None) or self._get_default_format() data = dict( realms=supported_realms, preferences=prefs, ) return "prefs_announcer_email.html", data
class Navigation(Component): """This plugin enables user to choose, if she uses a different (means more elegant way) display of naviagtion bar. Type of displaying menu. Possible types are: `normal` (default): No integration - ''leaves menu as in Trac standard'' `fixed_menu`: fixes menu incl. context navigation on top of browser window (under DEVELOPMENT!!), `buttom_ctx_menu`: adds context menu at buttom of page, if available """ implements(IRequestFilter, ITemplateProvider, ITemplateStreamFilter) display_navigation = ChoiceOption('trac', SESSION_KEYS['nav'], choices=DISPLAY_CHOICES, doc="""Type of displaying menu. Possible types are: `normal` (default): No integration - ''leaves menu as in Trac standard'' `fixed_menu`: fixes menu incl. context navigation on top of browser window (under DEVELOPMENT!!), `buttom_ctx_menu`: adds context menu at buttom of page, if available""") wiki_link = Option('mainnav', SESSION_KEYS['wiki'], default=None, doc='', doc_domain='tracini') ticket_link = Option('mainnav', SESSION_KEYS['tickets'], default=None, doc='', doc_domain='tracini') # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): display = self.get_display(req) if not display == 'normal': # only do something if specified if 'fixed_menu' == display: add_stylesheet(req, 'navpl/fixed_menu.css') return (template, data, content_type) # ITemplateProvider methods def get_templates_dirs(self): return def get_htdocs_dirs(self): return [('navpl', resource_filename(__name__, 'htdocs'))] # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): display = self.get_display(req) self.__inject_links(req, data['chrome']) if not display == 'normal': # non-standard" if 'fixed_menu' == display: return self._add_style_attributes(req, stream) elif 'buttom_ctx_menu' == display: return self._add_buttom_ctx_menu(req, stream) return stream def __inject_links(self, req, chrome_data): if chrome_data and chrome_data.has_key('nav'): nav = chrome_data['nav'] if nav and nav.has_key('mainnav'): repl_links = self.get_repl_links(req) if len(repl_links) > 0: mainnav = nav['mainnav'] for nav in mainnav: # self.log.info('nav: %s' % nav) if nav.has_key('name') \ and nav['name'] in repl_links.keys(): self.replace_links(req, nav, repl_links[nav['name']]) def replace_links(self, req, nav, new_link): wiki_label = nav['label'] name = '---' try: name = wiki_label.children if nav['name'] == 'wiki': nav['label'] = tag.a(name, href=req.href.wiki(new_link)) elif nav['name'] == 'tickets': if new_link == 'report': nav['label'] = tag.a(name, href=req.href.report()) elif new_link == 'query': nav['label'] = tag.a(name, href=req.href.query()) else: nav['label'] = tag.a(name, href=req.href.report(new_link)) except Exception, e: self.log.error(e)
class QueueDependencyAnalysis(Component, Analysis): """Building on mastertickets' blockedby relationships and the queue's position, this analyzes a report's tickets for two issues: 1. Detects when dependent tickets are in the wrong queue. 2. Detects when dependent tickets' positions are out of order. Specify which reports can be analyzed with the queue_reports option: [analyze] queue_reports = 1,2,3,9 In the example above, this analysis is available for reports 1, 2, 3 and 9. If no queue_reports is provided, then the queue's full list of reports will be used instead from the [queues] 'reports' option. The queue_fields config option is the list of fields that define a queue. You can optionally override with a report-specific option: [analyze] queue_fields = milestone,queue queue_fields.2 = queue queue_fields.9 = queue,phase!=verifying|readying In the example above, reports 1 and 3 are defined by fields 'milestone' and 'queue', report 2 is defined only by field 'queue', and report 9 is defined by field 'queue' as well as filtering the 'phase' field. The filtering spec should usually match those in the report - i.e., via a pipe-delimited list specify which tickets to include ('=') or not include ('!=') in the analysis.""" implements(IAnalysis) reports1 = ListOption('analyze', 'queue_reports', default=[], doc="Reports that can be queue dependency analyzed.") reports2 = ListOption('queues', 'reports', default=[], doc="Reports that can be queue dependency analyzed.") queue_fields = ListOption('analyze', 'queue_fields', default=[], doc="Ticket fields that define each queue.") audit = ChoiceOption( 'queues', 'audit', choices=['log', 'ticket', 'none'], doc="Record reorderings in log, in ticket, or not at all.") def can_analyze(self, report): # fallback to actual queue report list if not made explicit return report in (self.reports1 or self.reports2) def _add_args(self, args, report): """Split queue fields into standard and custom.""" queue_fields = self.env.config.get( 'analyze', 'queue_fields.' + report, self.queue_fields) # fallback if not report-specific if not isinstance(queue_fields, list): queue_fields = [f.strip() for f in queue_fields.split(',')] args['standard_fields'] = {} args['custom_fields'] = {} for name in queue_fields: vals = None if '=' in name: name, vals = name.split('=', 1) not_ = name.endswith('!') if not_: name = name[:-1] # save 'not' info at end of vals to pop off later vals = [v.strip() for v in vals.split('|')] + [not_] for field in TicketSystem(self.env).get_ticket_fields(): if name == field['name']: if 'custom' in field: args['custom_fields'][name] = vals else: args['standard_fields'][name] = vals break else: raise Exception("Unknown queue field: %s" % name) def get_solutions(self, db, args, report): if not args['col1_value1']: return '', [] # has no position so skip self._add_args(args, report) return queue.get_dependency_solutions(db, args) def fix_issue(self, db, data, author): """Honor queues audit config.""" if not isinstance(data, list): data = [data] # find position field for k, v in data[0].items(): if k == 'ticket': continue field = k if self.audit == 'ticket' or \ field in ('blocking','blockedby') or \ any(len(c) != 2 for c in data) or \ not self._isint(v): # heuristic for position field return Analysis.fix_issue(self, db, data, author) # honor audit config cursor = db.cursor() for changes in data: id = changes['ticket'] new_pos = changes[field] cursor.execute( """ SELECT value from ticket_custom WHERE name=%s AND ticket=%s """, (field, id)) result = cursor.fetchone() if result: old_pos = result[0] cursor.execute( """ UPDATE ticket_custom SET value=%s WHERE name=%s AND ticket=%s """, (new_pos, field, id)) else: old_pos = '(none)' cursor.execute( """ INSERT INTO ticket_custom (ticket,name,value) VALUES (%s,%s,%s) """, (id, field, new_pos)) if self.audit == 'log': self.log.info("%s reordered ticket #%s's %s from %s to %s" \ % (author,id,field,old_pos,new_pos)) db.commit()
class CkIntegrationModule(Component): """CKEditor integration for Trac Replace wiki-textareas in Trac with the CKEditor, as a fully-featured rich editor. Adds a request handler for AJAX-based TracWiki->HTML rendering. The plugin supports several modes of integration, determined by the `editor_type` option (see Configuration section). The CKEditor itself is not built into the plugin, in order to allow the administrator to choose the layout and configuration freely ('''note that CKEditor >= 3.6 is required'''). Use the `editor_source` option to determine the actual location of the editor. '''Disclaimer:''' This plugin is under development, and the `full_integration` mode is known to be experimental (at best) - only a handful of elements are supported. Feel free to join the effort to enhance the `full_integration` at http://trac-hacks.org/wiki/CkEditorPlugin. Configuration (config name, description, default values): [[TracIni(ckeditor)]]""" implements(ITemplateProvider, ITemplateStreamFilter, IRequestHandler) editor_type = ChoiceOption('ckeditor', 'editor_type', ['full_integration', 'only_ticket', 'only_wiki', 'html_wrapper', 'none'], """Type of integrated editor. Possible types are: `full_integration`: CKEditor with TracWiki output ('''experimental'''), `only_ticket`: CKEditor with TracWiki output for ticket fields ('''experimental'''); ''leaves wiki editing as in Trac standard'', `only_wiki`: CKEditor with TracWiki output for wiki pages ('''experimental'''); ''leaves ticket editing as in Trac standard'', `html_wrapper`: CKEditor with HTML output wrapped in html-processor, `none`: No integration - ''leaves editing as in Trac standard''""") editor_source = Option('ckeditor', 'editor_source', 'site/js/ckeditor/ckeditor.js', """Path to CKEditor 3.6.x javascript source. The path should be specified as viewed by the client, and must be accessible by the client-browser. A recommended setup involves installing CKEditor in the htdocs/js directory of the Trac environment, and setting this option to site/js/ckeditor/ckeditor.js.""") code_styles = ListOption('ckeditor', 'code_styles', 'cpp, csharp, java, js, python, sql, default, xml', doc="""List of code styles, which should be processed by CKEditor and displayed in CKEditor dialog 'insert code'.""") # editor_replace = Option('ckeditor', 'editor_replace', '', # """Javascript, which should replace textareas.""") template_fields = { 'ticket.html': ('field_description', 'comment', ), 'wiki_edit.html': ('text', ), 'admin_components.html': ('description', ), 'admin_milestones.html': ('description', ), 'admin_versions.html': ('description', ), } # ITemplateProvider def get_htdocs_dirs(self): from pkg_resources import resource_filename return [('ckintegration', resource_filename(__name__, 'htdocs'))] # ITemplateProvider def get_templates_dirs(self): return [] def _check_editor_type(self, filename): """Checks whether editor is enabled for this view (filename). Returns `true` if it is enabled, otherwise `false`. """ if not self.editor_type or 'none' == self.editor_type: return False elif 'only_ticket' == self.editor_type: return lower(filename) == 'ticket.html' elif 'only_wiki' == self.editor_type: return lower(filename) == 'wiki_edit.html' else: return lower(filename) in self.template_fields def get_styles_list(self): style_list = [ ] if self.code_styles: style_opt_list = self.code_styles self.log.info('self.code_styles: %s' % style_opt_list) for style in style_opt_list: if style == 'default': style_list.append(['Text', '']) continue try: from pygments.lexers import get_lexer_by_name lexer = get_lexer_by_name(style) style_list.append([lexer.name, style]) except Exception, e: self.log.warn( "Error when retrieving lexer by name: %s" % e ) return style_list
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 MasterTicketsModule(Component): """Provides support for ticket dependencies.""" implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter, ITemplateProvider, ITicketManipulator) dot_path = Option('mastertickets', 'dot_path', default='dot', doc='Path to the dot executable.') gs_path = Option('mastertickets', 'gs_path', default='gs', doc='Path to the ghostscript executable.') use_gs = BoolOption( 'mastertickets', 'use_gs', default=False, doc='If enabled, use ghostscript to produce nicer output.') closed_color = Option('mastertickets', 'closed_color', default='green', doc='Color of closed tickets') opened_color = Option('mastertickets', 'opened_color', default='red', doc='Color of opened tickets') show_key = Option('mastertickets', 'show_key', default=False, doc='Show a key for open/closed nodes') closed_text = Option('mastertickets', 'closed_text', default='Done', doc='Text for key showing closed tickets') opened_text = Option('mastertickets', 'opened_text', default='ToDo', doc='Text for key showing opened tickets') highlight_target = Option('mastertickets', 'highlight_target', default=False, doc='Highlight target tickets in graph') full_graph = Option( 'mastertickets', 'full_graph', default=False, doc='Show full dep. graph, not just direct blocking links') graph_direction = ChoiceOption( 'mastertickets', 'graph_direction', choices=['TD', 'LR', 'DT', 'RL'], doc= 'Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)' ) fields = set(['blocking', 'blockedby']) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if req.path_info.startswith('/ticket/'): # In case of an invalid ticket, the data is invalid if not data: return template, data, content_type tkt = data['ticket'] links = TicketLinks(self.env, tkt) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': add_script(req, 'mastertickets/disable_resolve.js') break # Add link to depgraph if needed if links: add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id)) for change in data.get('changes', {}): if not change.has_key('fields'): continue for field, field_data in change['fields'].iteritems(): if field in self.fields: if field_data['new'].strip(): new = set( [int(n) for n in field_data['new'].split(',')]) else: new = set() if field_data['old'].strip(): old = set( [int(n) for n in field_data['old'].split(',')]) else: old = set() add = new - old sub = old - new elms = tag() if add: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(add)]))) elms.append(u' added') if add and sub: elms.append(u'; ') if sub: elms.append( tag.em(u', '.join( [unicode(n) for n in sorted(sub)]))) elms.append(u' removed') field_data['rendered'] = elms #add a link to generate a dependency graph for all the tickets in the milestone if req.path_info.startswith('/milestone/'): if not data: return template, data, content_type milestone = data['milestone'] add_ctxtnav(req, 'Depgraph', req.href.depgraph('milestone', milestone.name)) return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if not data: return stream # We try all at the same time to maybe catch also changed or processed templates if filename in [ "report_view.html", "query_results.html", "ticket.html", "query.html" ]: # For ticket.html if 'fields' in data and isinstance(data['fields'], list): for field in data['fields']: for f in self.fields: if field['name'] == f and data['ticket'][f]: field['rendered'] = self._link_tickets( req, data['ticket'][f]) # For query_results.html and query.html if 'groups' in data and isinstance(data['groups'], list): for group, tickets in data['groups']: for ticket in tickets: for f in self.fields: if f in ticket: ticket[f] = self._link_tickets(req, ticket[f]) # For report_view.html if 'row_groups' in data and isinstance(data['row_groups'], list): for group, rows in data['row_groups']: for row in rows: if 'cell_groups' in row and isinstance( row['cell_groups'], list): for cells in row['cell_groups']: for cell in cells: # If the user names column in the report differently (blockedby AS "blocked by") then this will not find it if cell.get('header', {}).get('col') in self.fields: cell['value'] = self._link_tickets( req, cell['value']) return stream # ITicketManipulator methods def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): if req.args.get('action') == 'resolve' and req.args.get( 'action_resolve_resolve_resolution') == 'fixed': links = TicketLinks(self.env, ticket) for i in links.blocked_by: if Ticket(self.env, i)['status'] != 'closed': yield None, 'Ticket #%s is blocking this ticket' % i # ITemplateProvider methods def get_htdocs_dirs(self): """Return the absolute path of a directory containing additional static resources (such as images, style sheets, etc). """ return [('mastertickets', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): """Return the absolute path of the directory containing the provided ClearSilver templates. """ return [resource_filename(__name__, 'templates')] # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/depgraph') def process_request(self, req): path_info = req.path_info[10:] if not path_info: raise TracError('No ticket specified') #list of tickets to generate the depgraph for tkt_ids = [] milestone = None split_path = path_info.split('/', 2) #Urls to generate the depgraph for a ticket is /depgraph/ticketnum #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name if split_path[0] == 'milestone': #we need to query the list of tickets in the milestone milestone = split_path[1] query = Query(self.env, constraints={'milestone': [milestone]}, max=0) tkt_ids = [fields['id'] for fields in query.execute()] else: #the list is a single ticket tkt_ids = [int(split_path[0])] #the summary argument defines whether we place the ticket id or #it's summary in the node's label label_summary = 0 if 'summary' in req.args: label_summary = int(req.args.get('summary')) g = self._build_graph(req, tkt_ids, label_summary=label_summary) if path_info.endswith('/depgraph.png') or 'format' in req.args: format = req.args.get('format') if format == 'text': #in case g.__str__ returns unicode, we need to convert it in ascii req.send( to_unicode(g).encode('ascii', 'replace'), 'text/plain') elif format == 'debug': import pprint req.send( pprint.pformat( [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]), 'text/plain') elif format is not None: req.send(g.render(self.dot_path, format), 'text/plain') if self.use_gs: ps = g.render(self.dot_path, 'ps2') gs = subprocess.Popen([ self.gs_path, '-q', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4', '-sDEVICE=png16m', '-sOutputFile=%stdout%', '-' ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) img, err = gs.communicate(ps) if err: self.log.debug('MasterTickets: Error from gs: %s', err) else: img = g.render(self.dot_path) req.send(img, 'image/png') else: data = {} #add a context link to enable/disable labels in nodes if label_summary: add_ctxtnav(req, 'Without labels', req.href(req.path_info, summary=0)) else: add_ctxtnav(req, 'With labels', req.href(req.path_info, summary=1)) if milestone is None: tkt = Ticket(self.env, tkt_ids[0]) data['tkt'] = tkt add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id, req.href.ticket(tkt.id)) else: add_ctxtnav(req, 'Back to Milestone %s' % milestone, req.href.milestone(milestone)) data['milestone'] = milestone data['graph'] = g data['graph_render'] = partial(g.render, self.dot_path) data['use_gs'] = self.use_gs return 'depgraph.html', data, None def _build_graph(self, req, tkt_ids, label_summary=0): g = graphviz.Graph() g.label_summary = label_summary g.attributes['rankdir'] = self.graph_direction node_default = g['node'] node_default['style'] = 'filled' edge_default = g['edge'] edge_default['style'] = '' # Force this to the top of the graph for id in tkt_ids: g[id] if self.show_key: g[-1]['label'] = self.closed_text g[-1]['fillcolor'] = self.closed_color g[-1]['shape'] = 'box' g[-2]['label'] = self.opened_text g[-2]['fillcolor'] = self.opened_color g[-2]['shape'] = 'box' links = TicketLinks.walk_tickets(self.env, tkt_ids, full=self.full_graph) links = sorted(links, key=lambda link: link.tkt.id) for link in links: tkt = link.tkt node = g[tkt.id] if label_summary: node['label'] = u'#%s %s' % (tkt.id, tkt['summary']) else: node['label'] = u'#%s' % tkt.id node['fillcolor'] = tkt[ 'status'] == 'closed' and self.closed_color or self.opened_color node['URL'] = req.href.ticket(tkt.id) node['alt'] = u'Ticket #%s' % tkt.id node['tooltip'] = tkt['summary'] if self.highlight_target and tkt.id in tkt_ids: node['penwidth'] = 3 for n in link.blocking: node > g[n] return g def _link_tickets(self, req, tickets): items = [] for i, word in enumerate(re.split(r'([;,\s]+)', tickets)): if i % 2: items.append(word) elif word: ticketid = word word = '#%s' % word try: ticket = Ticket(self.env, ticketid) if 'TICKET_VIEW' in req.perm(ticket.resource): word = \ tag.a( '#%s' % ticket.id, class_=ticket['status'], href=req.href.ticket(int(ticket.id)), title=shorten_line(ticket['summary']) ) except ResourceNotFound: pass items.append(word) if items: return tag(items) else: return None
class RequestDispatcher(Component): """Web request dispatcher. This component dispatches incoming requests to registered handlers. Besides, it also takes care of user authentication and request pre- and post-processing. """ required = True implements(ITemplateProvider) authenticators = ExtensionPoint(IAuthenticator) handlers = ExtensionPoint(IRequestHandler) filters = OrderedExtensionsOption( 'trac', 'request_filters', IRequestFilter, doc="""Ordered list of filters to apply to all requests.""") default_handler = ExtensionOption( 'trac', 'default_handler', IRequestHandler, 'WikiModule', """Name of the component that handles requests to the base URL. Options include `TimelineModule`, `RoadmapModule`, `BrowserModule`, `QueryModule`, `ReportModule`, `TicketModule` and `WikiModule`.""") default_timezone = Option('trac', 'default_timezone', '', """The default timezone to use""") default_language = Option( 'trac', 'default_language', '', """The preferred language to use if no user preference has been set. (''since 0.12.1'') """) default_date_format = ChoiceOption( 'trac', 'default_date_format', ('', 'iso8601'), """The date format. Valid options are 'iso8601' for selecting ISO 8601 format, or leave it empty which means the default date format will be inferred from the browser's default language. (''since 1.0'') """) use_xsendfile = BoolOption( 'trac', 'use_xsendfile', 'false', """When true, send a `X-Sendfile` header and no content when sending files from the filesystem, so that the web server handles the content. This requires a web server that knows how to handle such a header, like Apache with `mod_xsendfile` or lighttpd. (''since 1.0'') """) xsendfile_header = Option( 'trac', 'xsendfile_header', 'X-Sendfile', """The header to use if `use_xsendfile` is enabled. If Nginx is used, set `X-Accel-Redirect`. (''since 1.0.6'')""") # Public API def authenticate(self, req): for authenticator in self.authenticators: try: authname = authenticator.authenticate(req) except TracError as e: self.log.error("Can't authenticate using %s: %s", authenticator.__class__.__name__, exception_to_unicode(e, traceback=True)) add_warning( req, _("Authentication error. " "Please contact your administrator.")) break # don't fallback to other authenticators if authname: return authname return 'anonymous' def dispatch(self, req): """Find a registered handler that matches the request and let it process it. In addition, this method initializes the data dictionary passed to the the template and adds the web site chrome. """ self.log.debug('Dispatching %r', req) chrome = Chrome(self.env) # Setup request callbacks for lazily-evaluated properties req.callbacks.update({ 'authname': self.authenticate, 'chrome': chrome.prepare_request, 'perm': self._get_perm, 'session': self._get_session, 'locale': self._get_locale, 'lc_time': self._get_lc_time, 'tz': self._get_timezone, 'form_token': self._get_form_token, 'use_xsendfile': self._get_use_xsendfile, 'xsendfile_header': self._get_xsendfile_header, }) try: try: # Select the component that should handle the request chosen_handler = None try: for handler in self._request_handlers.values(): if handler.match_request(req): chosen_handler = handler break if not chosen_handler and \ (not req.path_info or req.path_info == '/'): chosen_handler = self._get_valid_default_handler(req) # pre-process any incoming request, whether a handler # was found or not self.log.debug("Chosen handler is %s", chosen_handler) chosen_handler = \ self._pre_process_request(req, chosen_handler) except TracError as e: raise HTTPInternalError(e) if not chosen_handler: if req.path_info.endswith('/'): # Strip trailing / and redirect target = unicode_quote(req.path_info.rstrip('/')) if req.query_string: target += '?' + req.query_string req.redirect(req.href + target, permanent=True) raise HTTPNotFound('No handler matched request to %s', req.path_info) req.callbacks['chrome'] = partial(chrome.prepare_request, handler=chosen_handler) # Protect against CSRF attacks: we validate the form token # for all POST requests with a content-type corresponding # to form submissions if req.method == 'POST': ctype = req.get_header('Content-Type') if ctype: ctype, options = cgi.parse_header(ctype) if ctype in ('application/x-www-form-urlencoded', 'multipart/form-data') and \ req.args.get('__FORM_TOKEN') != req.form_token: if self.env.secure_cookies and req.scheme == 'http': msg = _('Secure cookies are enabled, you must ' 'use https to submit forms.') else: msg = _('Do you have cookies enabled?') raise HTTPBadRequest( _('Missing or invalid form token.' ' %(msg)s', msg=msg)) # Process the request and render the template resp = chosen_handler.process_request(req) if resp: if len(resp) == 2: # old Clearsilver template and HDF data self.log.error( "Clearsilver template are no longer " "supported (%s)", resp[0]) raise TracError( _("Clearsilver templates are no longer supported, " "please contact your Trac administrator.")) # Genshi template, data, content_type, method = \ self._post_process_request(req, *resp) if 'hdfdump' in req.args: req.perm.require('TRAC_ADMIN') # debugging helper - no need to render first out = io.BytesIO() pprint(data, out) req.send(out.getvalue(), 'text/plain') self.log.debug("Rendering response from handler") output = chrome.render_template( req, template, data, content_type, method=method, iterable=chrome.use_chunked_encoding) req.send(output, content_type or 'text/html') else: self.log.debug("Empty or no response from handler. " "Entering post_process_request.") self._post_process_request(req) except RequestDone: raise except: # post-process the request in case of errors err = sys.exc_info() try: self._post_process_request(req) except RequestDone: raise except Exception as e: self.log.error( "Exception caught while post-processing" " request: %s", exception_to_unicode(e, traceback=True)) raise err[0], err[1], err[2] except PermissionError as e: raise HTTPForbidden(e) except ResourceNotFound as e: raise HTTPNotFound(e) except TracError as e: raise HTTPInternalError(e) # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): return [pkg_resources.resource_filename('trac.web', 'templates')] # Internal methods @lazy def _request_handlers(self): return dict( (handler.__class__.__name__, handler) for handler in self.handlers) def _get_valid_default_handler(self, req): # Use default_handler from the Session if it is a valid value. name = req.session.get('default_handler') handler = self._request_handlers.get(name) if handler and not is_valid_default_handler(handler): handler = None if not handler: # Use default_handler from project configuration. handler = self.default_handler if not is_valid_default_handler(handler): raise ConfigurationError( tag_( "%(handler)s is not a valid default handler. Please " "update %(option)s through the %(page)s page or by " "directly editing trac.ini.", handler=tag.code(handler.__class__.__name__), option=tag.code("[trac] default_handler"), page=tag.a(_("Basic Settings"), href=req.href.admin('general/basics')))) return handler def _get_perm(self, req): if isinstance(req.session, FakeSession): return FakePerm() else: return PermissionCache(self.env, req.authname) def _get_session(self, req): try: return Session(self.env, req) except TracError as e: self.log.error("can't retrieve session: %s", exception_to_unicode(e)) return FakeSession() def _get_locale(self, req): if has_babel: preferred = req.session.get('language') default = self.env.config.get('trac', 'default_language', '') negotiated = get_negotiated_locale([preferred, default] + req.languages) self.log.debug("Negotiated locale: %s -> %s", preferred, negotiated) return negotiated def _get_lc_time(self, req): lc_time = req.session.get('lc_time') if not lc_time or lc_time == 'locale' and not has_babel: lc_time = self.default_date_format if lc_time == 'iso8601': return 'iso8601' return req.locale def _get_timezone(self, req): try: return timezone( req.session.get('tz', self.default_timezone or 'missing')) except Exception: return localtz def _get_form_token(self, req): """Used to protect against CSRF. The 'form_token' is strong shared secret stored in a user cookie. By requiring that every POST form to contain this value we're able to protect against CSRF attacks. Since this value is only known by the user and not by an attacker. If the the user does not have a `trac_form_token` cookie a new one is generated. """ if 'trac_form_token' in req.incookie: return req.incookie['trac_form_token'].value else: req.outcookie['trac_form_token'] = hex_entropy(24) req.outcookie['trac_form_token']['path'] = req.base_path or '/' if self.env.secure_cookies: req.outcookie['trac_form_token']['secure'] = True req.outcookie['trac_form_token']['httponly'] = True return req.outcookie['trac_form_token'].value def _get_use_xsendfile(self, req): return self.use_xsendfile # RFC7230 3.2 Header Fields _xsendfile_header_re = re.compile(r"[-0-9A-Za-z!#$%&'*+.^_`|~]+\Z") _warn_xsendfile_header = False def _get_xsendfile_header(self, req): header = self.xsendfile_header.strip() if self._xsendfile_header_re.match(header): return to_utf8(header) else: if not self._warn_xsendfile_header: self._warn_xsendfile_header = True self.log.warn("[trac] xsendfile_header is invalid: '%s'", header) return None def _pre_process_request(self, req, chosen_handler): for filter_ in self.filters: chosen_handler = filter_.pre_process_request(req, chosen_handler) return chosen_handler def _post_process_request(self, req, *args): resp = args # `method` is optional in IRequestHandler's response. If not # specified, the default value is appended to response. if len(resp) == 3: resp += (None, ) nbargs = len(resp) for f in reversed(self.filters): # As the arity of `post_process_request` has changed since # Trac 0.10, only filters with same arity gets passed real values. # Errors will call all filters with None arguments, # and results will not be not saved. extra_arg_count = arity(f.post_process_request) - 1 if extra_arg_count == nbargs: resp = f.post_process_request(req, *resp) elif extra_arg_count == nbargs - 1: # IRequestFilters may modify the `method`, but the `method` # is forwarded when not accepted by the IRequestFilter. method = resp[-1] resp = f.post_process_request(req, *resp[:-1]) resp += (method, ) elif nbargs == 0: f.post_process_request(req, *(None, ) * extra_arg_count) return resp
class LDAPStore(Component): """An AccountManager backend to use LDAP.""" host_url = Option('ldap', 'host_url', doc='Server URL to use for LDAP authentication') base_dn = Option('ldap', 'base_dn', doc='The user search base') bind_user = Option('ldap', 'bind_user', doc='LDAP user for searching') bind_password = Option('ldap', 'bind_password', doc='LDAP user password') search_scope = ChoiceOption('ldap', 'search_scope', SEARCH_SCOPES, doc='The ldap search scope: base, onelevel or ' 'subtree') search_filter = Option('ldap', 'search_filter', default='(&(objectClass=user)(sAMAccountName=%s))', doc='The ldap search filter template where %%s is ' 'replace with the username') implements(IPasswordStore) def check_password(self, user, password): self.log.debug('LDAPAuth: Checking password for user %s', user) conn = None try: conn = self._create_ldap_conn() if conn is not None and self._authenticate(conn, self.bind_user, self.bind_password): bind_cn = self._search_user(conn, user) if bind_cn is not None and self._authenticate( conn, bind_cn, password): return PASSWORDSTORE_SUCCESS else: return PASSWORDSTORE_FAILURE else: return PASSWORDSTORE_FALLTHROUGH except Exception: self.log.debug('LDAPAuth: Unexpected error: %s', traceback.format_exc()) finally: if conn is not None: conn.unbind_s() return PASSWORDSTORE_FALLTHROUGH def get_users(self): # TODO: investigate how to get LDAP users that successfully logged in return [] def has_user(self, user): return False def _create_ldap_conn(self): """Creates an LDAP connection""" self.log.debug('LDAPAuth: Initializing LDAP connection for %s', self.host_url) conn = None try: conn = ldap.initialize(self.host_url) if self._should_use_tls(): self.log.debug('LDAPAuth: starting TLS') conn.start_tls_s() except ldap.LDAPError, e: self.log.debug('LDAPAuth: Could not create connection: %s', e) return conn