def _get_authz_info(self): if not self.authz_file: self.log.error("The [svn] authz_file configuration option in " "trac.ini is empty or not defined") raise ConfigurationError() try: mtime = os.path.getmtime(self.authz_file) except OSError as e: self.log.error("Error accessing svn authz permission policy " "file: %s", exception_to_unicode(e)) raise ConfigurationError() if mtime != self._mtime: self._mtime = mtime rm = RepositoryManager(self.env) modules = set(repos.reponame for repos in rm.get_real_repositories()) if '' in modules and self.authz_module_name: modules.add(self.authz_module_name) modules.add('') self.log.info("Parsing authz file: %s", self.authz_file) try: self._authz = parse(self.authz_file, modules) except ParsingError as e: self.log.error("Error parsing svn authz permission policy " "file: %s", exception_to_unicode(e)) raise ConfigurationError() else: self._users = {user for paths in self._authz.itervalues() for path in paths.itervalues() for user, result in path.iteritems() if result} return self._authz, self._users
def parse_authz(self): if ConfigObj is None: self.log.error('ConfigObj package not found.') raise ConfigurationError() self.log.debug('Parsing authz security policy %s', self.get_authz_file) try: self.authz = ConfigObj(self.get_authz_file, encoding='utf8', raise_errors=True) except ConfigObjError as e: self.log.error("Error parsing authz permission policy file: %s", to_unicode(e)) raise ConfigurationError() groups = {} for group, users in self.authz.get('groups', {}).iteritems(): if isinstance(users, basestring): users = [users] groups[group] = map(to_unicode, users) self.groups_by_user = {} def add_items(group, items): for item in items: if item.startswith('@'): add_items(group, groups[item[1:]]) else: self.groups_by_user.setdefault(item, set()).add(group) for group, users in groups.iteritems(): add_items('@' + group, users) self.authz_mtime = os.path.getmtime(self.get_authz_file)
def parse_authz(self): self.log.debug("Parsing authz security policy %s", self.authz_file) if not self.authz_file: self.log.error("The `[authz_policy] authz_file` configuration " "option in trac.ini is empty or not defined.") raise ConfigurationError() try: authz_mtime = os.path.getmtime(self.authz_file) except OSError as e: self.log.error("Error parsing authz permission policy file: %s", exception_to_unicode(e)) raise ConfigurationError() self.authz = UnicodeConfigParser(ignorecase_option=False) try: self.authz.read(self.authz_file) except configparser.ParsingError as e: self.log.error("Error parsing authz permission policy file: %s", exception_to_unicode(e)) raise ConfigurationError() groups = {} if self.authz.has_section('groups'): for group, users in self.authz.items('groups'): groups[group] = to_list(users) self.groups_by_user = {} def add_items(group, items): for item in items: if item.startswith('@'): add_items(group, groups[item[1:]]) else: self.groups_by_user.setdefault(item, set()).add(group) for group, users in groups.items(): add_items('@' + group, users) all_actions = set(PermissionSystem(self.env).get_actions()) authz_basename = os.path.basename(self.authz_file) for section in self.authz.sections(): if section == 'groups': continue for user, actions in self.authz.items(section): for action in to_list(actions): if action.startswith('!'): action = action[1:] if action not in all_actions: self.log.warning( "The action %s in the [%s] section " "of %s is not a valid action.", action, section, authz_basename) self.authz_mtime = authz_mtime
def __init__(self): if not self.authz_file: self.log.error("The `[authz_policy] authz_file` configuration " "option in trac.ini is empty or not defined.") raise ConfigurationError() try: os.stat(self.authz_file) except OSError as e: self.log.error("Error parsing authz permission policy file: %s", to_unicode(e)) raise ConfigurationError() self.groups_by_user = {}
def parse_authz(self): self.log.debug("Parsing authz security policy %s", self.authz_file) self.authz = UnicodeConfigParser() try: self.authz.read(self.authz_file) except ParsingError as e: self.log.error("Error parsing authz permission policy file: %s", to_unicode(e)) raise ConfigurationError() groups = {} if self.authz.has_section('groups'): for group, users in self.authz.items('groups'): groups[group] = to_list(users) self.groups_by_user = {} def add_items(group, items): for item in items: if item.startswith('@'): add_items(group, groups[item[1:]]) else: self.groups_by_user.setdefault(item, set()).add(group) for group, users in groups.iteritems(): add_items('@' + group, users) self.authz_mtime = os.path.getmtime(self.authz_file)
def get_authz_file(self): if not self.authz_file: self.log.error('The `[authz_policy] authz_file` configuration ' 'option in trac.ini is empty or not defined.') raise ConfigurationError() authz_file = self.authz_file if os.path.isabs(self.authz_file) \ else os.path.join(self.env.path, self.authz_file) try: os.stat(authz_file) except OSError as e: self.log.error("Error parsing authz permission policy file: %s", to_unicode(e)) raise ConfigurationError() return authz_file
def __init__(self): solr_url = self.config.get('pysolr_search_backend', 'solr_url', None) timeout = self.config.getfloat('pysolr_search_backend', 'timeout', 30) if not solr_url: raise ConfigurationError( 'PySolrSearchBackend must be configured in trac.ini') self.conn = pysolr.Solr(solr_url, timeout=timeout)
def create_message_id(env, targetid, from_email, time, more=None): """Generate a predictable, but sufficiently unique message ID. In case you want to set the "Message ID" header, this convenience function will generate one by running a hash algorithm over a number of properties. :param env: the `Environment` :param targetid: a string that identifies the target, like `NotificationEvent.target` :param from_email: the email address that the message is sent from :param time: a Python `datetime` :param more: a string that contains additional information that makes this message unique """ items = [env.project_url, targetid, to_utimestamp(time)] if more is not None: items.append(more.encode('ascii', 'ignore')) source = b'.'.join( item if isinstance(item, bytes) else str(item).encode('utf-8') for item in items) hash_type = NotificationSystem(env).message_id_hash try: h = hashlib.new(hash_type) except: raise ConfigurationError( _("Unknown hash type '%(type)s'", type=hash_type)) h.update(source) host = from_email[from_email.find('@') + 1:] return '<%03d.%s@%s>' % (len(source), h.hexdigest(), host)
def send(self, from_addr, recipients, message): # Use native line endings in message message = fix_eol(message, os.linesep) self.log.info("Sending notification through sendmail at %s to %s", self.sendmail_path, recipients) cmdline = [self.sendmail_path, '-i', '-f', from_addr] + recipients self.log.debug("Sendmail command line: %s", cmdline) try: child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds) except OSError as e: raise ConfigurationError( tag_( "Sendmail error (%(error)s). Please modify %(option)s " "in your configuration.", error=to_unicode(e), option=tag.code("[notification] sendmail_path"))) out, err = child.communicate(message) if child.returncode or err: raise Exception("Sendmail failed with (%s, %s), command: '%s'" % (child.returncode, err.strip(), cmdline))
def _invalid_db_str(db_str): return ConfigurationError( tag_( "Invalid format %(db_str)s for the database connection string. " "Please refer to the %(doc)s for help.", db_str=tag.code(db_str), doc=_doc_db_str()))
def __init__(self, path, log=None, params={}): if have_pysqlite == 0: raise TracError(_("Cannot load Python bindings for SQLite")) self.cnx = None if path != ':memory:': if not os.access(path, os.F_OK): raise ConfigurationError( _('Database "%(path)s" not found.', path=path)) dbdir = os.path.dirname(path) if not os.access(path, os.R_OK + os.W_OK) or \ not os.access(dbdir, os.R_OK + os.W_OK): raise ConfigurationError( tag_( "The user %(user)s requires read _and_ write permissions " "to the database file %(path)s and the directory it is " "located in.", user=tag.tt(getuser()), path=tag.tt(path))) self._active_cursors = weakref.WeakKeyDictionary() timeout = int(params.get('timeout', 10.0)) self._eager = params.get('cursor', 'eager') == 'eager' # eager is default, can be turned off by specifying ?cursor= if isinstance(path, unicode): # needed with 2.4.0 path = path.encode('utf-8') cnx = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES, isolation_level=None, check_same_thread=sqlite_version < (3, 3, 1), timeout=timeout) # load extensions extensions = params.get('extensions', []) if len(extensions) > 0: cnx.enable_load_extension(True) for ext in extensions: cnx.load_extension(ext) cnx.enable_load_extension(False) cursor = cnx.cursor() _set_journal_mode(cursor, params.get('journal_mode')) _set_synchronous(cursor, params.get('synchronous')) cursor.close() cnx.isolation_level = 'DEFERRED' ConnectionWrapper.__init__(self, cnx, log)
def getfloat(self, name, default=''): value = self.get(name, default) if not value: return 0. try: return float(value) except ValueError: raise ConfigurationError( '[%(section)s] %(entry)s: expected float, got %(value)s' % dict(section=self.name, entry=name, value=repr(value)))
def _get_abspath(self, key, value): value.rstrip('/') if not os.path.isabs(value): raise ConfigurationError( '[%(section)s] %(entry)s: expected abs path, got %(value)s', { 'section': self.section, 'entry': key, 'value': repr(value) }) return os.path.normcase(value)
def _get_directory(self, key, value): if '/' in value or '\\' in value or '..' in value: raise ConfigurationError( '[%(section)s] %(entry)s: expected directory name, ' 'got %(value)s' % { 'section': self.section, 'entry': key, 'value': repr(value) }) return value
def send(self, from_addr, recipients, message): global local_hostname # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port, local_hostname) local_hostname = server.local_hostname except smtplib.socket.error as e: raise ConfigurationError( tag_( "SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError( _("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time_now() server.sendmail(from_addr, recipients, message) t = time_now() - start if t > 5: self.log.warning( "Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit()
def create_message_id(env, targetid, from_email, time, more=None): """Generate a predictable, but sufficiently unique message ID.""" items = [env.project_url.encode('utf-8'), targetid, to_utimestamp(time)] if more is not None: items.append(more.encode('ascii', 'ignore')) source = '.'.join(str(item) for item in items) hash_type = NotificationSystem(env).message_id_hash try: h = hashlib.new(hash_type) except: raise ConfigurationError( _("Unknown hash type '%(type)s'", type=hash_type)) h.update(source) host = from_email[from_email.find('@') + 1:] return '<%03d.%s@%s>' % (len(source), h.hexdigest(), host)
def accessor(self, section, name, default=''): """Return the value of the specified option as float. If the specified option can not be converted to a float, a `ConfigurationError` exception is raised. Valid default input is a string or a float. Returns an float. """ value = section.get(name, default) if not value: return 0.0 try: return float(value) except ValueError: raise ConfigurationError('expected real number, got %s' % \ repr(value))
def __init__(self): solr_url = self.config.get(*CONFIG_FIELD['solr_url']) timeout = self.config.getfloat(*CONFIG_FIELD['timeout']) if not solr_url: raise ConfigurationError( 'PySolrSearchBackend must be configured in trac.ini') self.conn = pysolr.Solr(solr_url, timeout=timeout) self.async_indexing = self.config.getbool( *CONFIG_FIELD['async_indexing']) if self.async_indexing: maxsize = self.config.getint(*CONFIG_FIELD['async_queue_maxsize']) self.indexer = AsyncSolrIndexer(self, maxsize) self.indexer.start() else: self.indexer = SolrIndexer(self)
def send(self, from_addr, recipients, message): # Use native line endings in message message = fix_eol(message, os.linesep) self.log.info("Sending notification through sendmail at %s to %s", self.sendmail_path, recipients) cmdline = [self.sendmail_path, "-i", "-f", from_addr] cmdline.extend(recipients) self.log.debug("Sendmail command line: %s", cmdline) try: child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds) except OSError, e: raise ConfigurationError( tag_("Sendmail error (%(error)s). Please modify %(option)s " "in your configuration.", error=to_unicode(e), option=tag.tt("[notification] sendmail_path")))
def send(self, from_addr, recipients, message): global local_hostname # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port, local_hostname) local_hostname = server.local_hostname except smtplib.socket.error, e: raise ConfigurationError( tag_("SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.tt("[notification] smtp_server"), option2=tag.tt("[notification] smtp_port")))
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_relative_url(self, key, value): value = urlparse(value).path if not value: raise ConfigurationError( '[%(section)s] %(entry)s: expected relative url, ' 'got %(value)s', { 'section': self.section, 'entry': key, 'value': repr(value) }) if value.strip('/') != value: conf.log.warning( '[%(section)s] %(entry)s: value %(value)s is invalid, ' 'it should not have leading or trailing slashes', { 'section': self.section, 'entry': key, 'value': repr(value) }) value = value.strip('/') return value
def _raise_value_error(self, option, typestring, value): """Helper for Macro*Option classes to raise an appropriate value error when a malformed value is supplied for an option.""" qual = option._qualified_name() if qual[0] == 'trac.ini': raise ConfigurationError( _('trac.ini [%(sec)s] %(opt)s = "%(val)s": invalid %(type)s', sec=self.section, opt=qual[1], type=typestring, val=repr(value))) if qual[0] == 'macroarg': raise ValueError( _('macro argument %(opt)s = "%(val)s": invalid %(type)s', opt=qual[1], type=typestring, val=repr(value))) if qual[0] == 'default': raise TracError( _('plugin default %(opt)s = "%(val)s": invalid %(type)s', opt=qual[1], type=typestring, val=repr(value)))
def render_ticket_action_control(self, req, ticket, action): self.log.debug('render_ticket_action_control: action "%s"', action) this_action = self.actions[action] status = this_action['newstate'] operations = this_action['operations'] ticket_owner = ticket._old.get('owner', ticket['owner']) ticket_status = ticket._old.get('status', ticket['status']) author = get_reporter_id(req, 'author') author_info = partial(Chrome(self.env).authorinfo, req, resource=ticket.resource) format_author = partial(Chrome(self.env).format_author, req, resource=ticket.resource) formatted_current_owner = author_info(ticket_owner) exists = ticket_status is not None ticket_system = TicketSystem(self.env) control = [] # default to nothing hints = [] if 'reset_workflow' in operations: control.append(_("from invalid state")) hints.append(_("Current state no longer exists")) if 'del_owner' in operations: hints.append(_("The ticket will be disowned")) if 'set_owner' in operations or 'may_set_owner' in operations: owners = self.get_allowed_owners(req, ticket, this_action) if 'set_owner' in operations: default_owner = author elif 'may_set_owner' in operations: if not exists: default_owner = ticket_system.default_owner else: default_owner = ticket_owner or None if owners is not None and default_owner not in owners: owners.insert(0, default_owner) else: # Protect against future modification for case that another # operation is added to the outer conditional raise AssertionError(operations) id = 'action_%s_reassign_owner' % action if not owners: owner = req.args.get(id, default_owner) control.append( tag_("to %(owner)s", owner=tag.input(type='text', id=id, name=id, value=owner))) if not exists or ticket_owner is None: hints.append(_("The owner will be the specified user")) else: hints.append(tag_("The owner will be changed from " "%(current_owner)s to the specified " "user", current_owner=formatted_current_owner)) elif len(owners) == 1: owner = tag.input(type='hidden', id=id, name=id, value=owners[0]) formatted_new_owner = author_info(owners[0]) control.append(tag_("to %(owner)s", owner=tag(formatted_new_owner, owner))) if not exists or ticket_owner is None: hints.append(tag_("The owner will be %(new_owner)s", new_owner=formatted_new_owner)) elif ticket['owner'] != owners[0]: hints.append(tag_("The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_new_owner)) else: selected_owner = req.args.get(id, default_owner) control.append(tag_("to %(owner)s", owner=tag.select( [tag.option(label, value=value if value is not None else '', selected=(value == selected_owner or None)) for label, value in sorted((format_author(owner), owner) for owner in owners)], id=id, name=id))) if not exists or ticket_owner is None: hints.append(_("The owner will be the selected user")) else: hints.append(tag_("The owner will be changed from " "%(current_owner)s to the selected user", current_owner=formatted_current_owner)) elif 'set_owner_to_self' in operations: formatted_author = author_info(author) if not exists or ticket_owner is None: hints.append(tag_("The owner will be %(new_owner)s", new_owner=formatted_author)) elif ticket_owner != author: hints.append(tag_("The owner will be changed from " "%(current_owner)s to %(new_owner)s", current_owner=formatted_current_owner, new_owner=formatted_author)) elif ticket_status != status: hints.append(tag_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner)) if 'set_resolution' in operations: resolutions = [r.name for r in Resolution.select(self.env)] if 'set_resolution' in this_action: valid_resolutions = set(resolutions) resolutions = this_action['set_resolution'] if any(x not in valid_resolutions for x in resolutions): raise ConfigurationError(_( "Your workflow attempts to set a resolution but uses " "undefined resolutions (configuration issue, please " "contact your Trac admin).")) if not resolutions: raise ConfigurationError(_( "Your workflow attempts to set a resolution but none is " "defined (configuration issue, please contact your Trac " "admin).")) id = 'action_%s_resolve_resolution' % action if len(resolutions) == 1: resolution = tag.input(type='hidden', id=id, name=id, value=resolutions[0]) control.append(tag_("as %(resolution)s", resolution=tag(resolutions[0], resolution))) hints.append(tag_("The resolution will be set to %(name)s", name=resolutions[0])) else: selected_option = req.args.get(id, ticket_system.default_resolution) control.append(tag_("as %(resolution)s", resolution=tag.select( [tag.option(x, value=x, selected=(x == selected_option or None)) for x in resolutions], id=id, name=id))) hints.append(_("The resolution will be set")) if 'del_resolution' in operations: hints.append(_("The resolution will be deleted")) if 'leave_status' in operations: control.append(tag_("as %(status)s", status=ticket_status)) if len(operations) == 1: hints.append(tag_("The owner will remain %(current_owner)s", current_owner=formatted_current_owner) if ticket_owner else _("The ticket will remain with no owner")) elif not operations: if status != '*': if ticket['status'] is None: hints.append(tag_("The status will be '%(name)s'", name=status)) else: hints.append(tag_("Next status will be '%(name)s'", name=status)) return (this_action['label'], tag(separated(control, ' ')), tag(separated(hints, '. ', '.') if hints else ''))
def accessor(self, section, name, default): value = section.get(name, default) if value not in self.choices: raise ConfigurationError('expected a choice among "%s", got %s' % \ (', '.join(self.choices), repr(value))) return value
def parse_connection_uri(db_str): """Parse the database connection string. The database connection string for an environment is specified through the `database` option in the `[trac]` section of trac.ini. :return: a tuple containing the scheme and a dictionary of attributes: `user`, `password`, `host`, `port`, `path`, `params`. :since: 1.1.3 """ if not db_str: section = tag.a("[trac]", title=_("TracIni documentation"), class_='trac-target-new', href='https://trac.edgewall.org/wiki/TracIni' '#trac-section') raise ConfigurationError( tag_( "Database connection string is empty. Set the %(option)s " "configuration option in the %(section)s section of " "trac.ini. Please refer to the %(doc)s for help.", option=tag.code("database"), section=section, doc=_doc_db_str())) try: scheme, rest = db_str.split(':', 1) except ValueError: raise _invalid_db_str(db_str) if not rest.startswith('/'): if scheme == 'sqlite' and rest: # Support for relative and in-memory SQLite connection strings host = None path = rest else: raise _invalid_db_str(db_str) else: if not rest.startswith('//'): host = None rest = rest[1:] elif rest.startswith('///'): host = None rest = rest[3:] else: rest = rest[2:] if '/' in rest: host, rest = rest.split('/', 1) else: host = rest rest = '' path = None if host and '@' in host: user, host = host.split('@', 1) if ':' in user: user, password = user.split(':', 1) else: password = None if user: user = urllib.unquote(user) if password: password = unicode_passwd(urllib.unquote(password)) else: user = password = None if host and ':' in host: host, port = host.split(':', 1) try: port = int(port) except ValueError: raise _invalid_db_str(db_str) else: port = None if not path: path = '/' + rest if os.name == 'nt': # Support local paths containing drive letters on Win32 if len(rest) > 1 and rest[1] == '|': path = "%s:%s" % (rest[0], rest[2:]) params = {} if '?' in path: path, qs = path.split('?', 1) qs = qs.split('&') for param in qs: try: name, value = param.split('=', 1) except ValueError: raise _invalid_db_str(db_str) value = urllib.unquote(value) params[name] = value args = zip(('user', 'password', 'host', 'port', 'path', 'params'), (user, password, host, port, path, params)) return scheme, {key: value for key, value in args if value}
def send(self, from_addr, recipients, message): # Ensure the message complies with RFC2822: use CRLF line endings message = fix_eol(message, CRLF) self.log.info("Sending notification through SMTP at %s:%d to %s", self.smtp_server, self.smtp_port, recipients) try: server = smtplib.SMTP(self.smtp_server, self.smtp_port) except smtplib.socket.error as e: raise ConfigurationError( tag_( "SMTP server connection error (%(error)s). Please " "modify %(option1)s or %(option2)s in your " "configuration.", error=to_unicode(e), option1=tag.code("[notification] smtp_server"), option2=tag.code("[notification] smtp_port"))) # server.set_debuglevel(True) if self.use_tls: server.ehlo() if 'starttls' not in server.esmtp_features: raise TracError( _("TLS enabled but server does not support" " TLS")) server.starttls() server.ehlo() if self.smtp_user: server.login(self.smtp_user.encode('utf-8'), self.smtp_password.encode('utf-8')) start = time.time() resp = sendmail(server, from_addr, recipients, message) t = time.time() - start if t > 5: self.log.warning( "Slow mail submission (%.2f s), " "check your mail setup", t) if self.use_tls: # avoid false failure detection when the server closes # the SMTP connection with TLS enabled import socket try: server.quit() except socket.sslerror: pass else: server.quit() msg = email.message_from_string(message) ticket_id = int(msg['x-trac-ticket-id']) msgid = msg['message-id'] aws_re = r'^email-smtp\.([a-z0-9-]+)\.amazonaws\.com$' m = re.match(aws_re, self.smtp_server) if m: parts = resp.split() if len(parts) == 2 and parts[0] == 'Ok': region = m.group(1) msgid = '<%s@%s.amazonses.com>' % (parts[1], region) with self.env.db_transaction as db: cursor = db.cursor() cursor.execute( """ INSERT OR IGNORE INTO messageid (ticket,messageid) VALUES (%s, %s) """, (ticket_id, msgid))
def check_config(self): if not self.public_key or not self.private_key: raise ConfigurationError('public_key and private_key needs ' \ 'to be in the [recaptcha] section of your trac.ini file. ' \ 'Get these keys from http://recaptcha.net/')