Ejemplo n.º 1
0
def resolve_ep_class(interface, component, clsnm, **kwargs):
    r"""Retrieve the class implementing an interface (by name)
    """
    ep = ExtensionPoint(interface)
    for c in ep.extensions(component):
        if c.__class__.__name__ == clsnm :
            return c
    else:
        if 'default' in kwargs:
            return kwargs['default']
        else:
            raise LookupError('No match found for class %s implementing %s' % 
                    (clsnm, interface) )
Ejemplo n.º 2
0
class OrderedExtensionsOption(ListOption):
    """A comma separated, ordered, list of components implementing `interface`.
    Can be empty.

    If `include_missing` is true (the default) all components implementing the
    interface are returned, with those specified by the option ordered first."""

    def __init__(self, section, name, interface, default=None,
                 include_missing=True, doc=''):
        ListOption.__init__(self, section, name, default, doc=doc)
        self.xtnpt = ExtensionPoint(interface)
        self.include_missing = include_missing

    def __get__(self, instance, owner):
        if instance is None:
            return self
        order = ListOption.__get__(self, instance, owner)
        components = []
        for impl in self.xtnpt.extensions(instance):
            if self.include_missing or impl.__class__.__name__ in order:
                components.append(impl)

        def compare(x, y):
            x, y = x.__class__.__name__, y.__class__.__name__
            if x not in order:
                return int(y in order)
            if y not in order:
                return -int(x in order)
            return cmp(order.index(x), order.index(y))
        components.sort(compare)
        return components
Ejemplo n.º 3
0
class ExtensionOption(Option):
    """Name of a component implementing `interface`. Raises a
    `ConfigurationError` if the component cannot be found in the list of
    active components implementing the interface."""

    def __init__(self, section, name, interface, default=None, doc='',
                 doc_domain='tracini'):
        Option.__init__(self, section, name, default, doc, doc_domain)
        self.xtnpt = ExtensionPoint(interface)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = Option.__get__(self, instance, owner)
        for impl in self.xtnpt.extensions(instance):
            if impl.__class__.__name__ == value:
                return impl
        raise ConfigurationError(
            tag_("Cannot find an implementation of the %(interface)s "
                 "interface named %(implementation)s. Please check "
                 "that the Component is enabled or update the option "
                 "%(option)s in trac.ini.",
                 interface=tag.code(self.xtnpt.interface.__name__),
                 implementation=tag.code(value),
                 option=tag.code("[%s] %s" % (self.section, self.name))))
Ejemplo n.º 4
0
class OrderedExtensionsOption(ListOption):
    """A comma separated, ordered, list of components implementing
    `interface`. Can be empty.

    If `include_missing` is true (the default) all components implementing the
    interface are returned, with those specified by the option ordered first.
    """

    def __init__(self, section, name, interface, default=None,
                 include_missing=True, doc='', doc_domain='tracini'):
        ListOption.__init__(self, section, name, default, doc=doc,
                            doc_domain=doc_domain)
        self.xtnpt = ExtensionPoint(interface)
        self.include_missing = include_missing

    def __get__(self, instance, owner):
        if instance is None:
            return self
        order = ListOption.__get__(self, instance, owner)
        components = []
        implementing_classes = []
        for impl in self.xtnpt.extensions(instance):
            implementing_classes.append(impl.__class__.__name__)
            if self.include_missing or impl.__class__.__name__ in order:
                components.append(impl)
        not_found = sorted(set(order) - set(implementing_classes))
        if not_found:
            raise ConfigurationError(
                tag_("Cannot find implementation(s) of the %(interface)s "
                     "interface named %(implementation)s. Please check "
                     "that the Component is enabled or update the option "
                     "%(option)s in trac.ini.",
                     interface=tag.code(self.xtnpt.interface.__name__),
                     implementation=tag(
                         (', ' if idx != 0 else None, tag.code(impl))
                         for idx, impl in enumerate(not_found)),
                     option=tag.code("[%s] %s" % (self.section, self.name))))

        def compare(x, y):
            x, y = x.__class__.__name__, y.__class__.__name__
            if x not in order:
                return int(y in order)
            if y not in order:
                return -int(x in order)
            return cmp(order.index(x), order.index(y))
        components.sort(compare)
        return components
Ejemplo n.º 5
0
class ExtensionOption(Option):

    def __init__(self, section, name, interface, default=None, doc=''):
        Option.__init__(self, section, name, default, doc)
        self.xtnpt = ExtensionPoint(interface)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = Option.__get__(self, instance, owner)
        for impl in self.xtnpt.extensions(instance):
            if impl.__class__.__name__ == value:
                return impl
        raise AttributeError('Cannot find an implementation of the "%s" '
                             'interface named "%s".  Please update the option '
                             '%s.%s in trac.ini.'
                             % (self.xtnpt.interface.__name__, value,
                                self.section, self.name))
Ejemplo n.º 6
0
class EmailDistributor(Component):
    """Distributes notification events as emails."""

    implements(INotificationDistributor)

    formatters = ExtensionPoint(INotificationFormatter)
    decorators = ExtensionPoint(IEmailDecorator)

    resolvers = OrderedExtensionsOption(
        'notification',
        'email_address_resolvers',
        IEmailAddressResolver,
        'SessionEmailResolver',
        include_missing=False,
        doc="""Comma separated list of email resolver components in the order
        they will be called.  If an email address is resolved, the remaining
        resolvers will not be called.
        """)

    default_format = Option(
        'notification',
        'default_format.email',
        'text/plain',
        doc="Default format to distribute email notifications.")

    def __init__(self):
        self._charset = create_charset(
            self.config.get('notification', 'mime_encoding'))

    # INotificationDistributor methods

    def transports(self):
        yield 'email'

    def distribute(self, transport, recipients, event):
        if transport != 'email':
            return
        if not self.config.getbool('notification', 'smtp_enabled'):
            self.log.debug("%s skipped because smtp_enabled set to false",
                           self.__class__.__name__)
            return

        formats = {}
        for f in self.formatters:
            for style, realm in f.get_supported_styles(transport):
                if realm == event.realm:
                    formats[style] = f
        if not formats:
            self.log.error("%s No formats found for %s %s",
                           self.__class__.__name__, transport, event.realm)
            return
        self.log.debug(
            "%s has found the following formats capable of "
            "handling '%s' of '%s': %s", self.__class__.__name__, transport,
            event.realm, ', '.join(formats))

        matcher = RecipientMatcher(self.env)
        notify_sys = NotificationSystem(self.env)
        always_cc = set(notify_sys.smtp_always_cc_list)
        addresses = {}
        for sid, auth, addr, fmt in recipients:
            if fmt not in formats:
                self.log.debug("%s format %s not available for %s %s",
                               self.__class__.__name__, fmt, transport,
                               event.realm)
                continue

            if sid and not addr:
                for resolver in self.resolvers:
                    addr = resolver.get_address_for_session(sid, auth) or None
                    if addr:
                        self.log.debug(
                            "%s found the address '%s' for '%s [%s]' via %s",
                            self.__class__.__name__, addr, sid, auth,
                            resolver.__class__.__name__)
                        break
            if sid and auth and not addr:
                addr = sid
            if notify_sys.smtp_default_domain and \
                    not notify_sys.use_short_addr and \
                    addr and matcher.nodomaddr_re.match(addr):
                addr = '%s@%s' % (addr, notify_sys.smtp_default_domain)
            if not addr:
                self.log.debug(
                    "%s was unable to find an address for "
                    "'%s [%s]'", self.__class__.__name__, sid, auth)
            elif matcher.is_email(addr) or \
                    notify_sys.use_short_addr and \
                    matcher.nodomaddr_re.match(addr):
                addresses.setdefault(fmt, set()).add(addr)
                if sid and auth and sid in always_cc:
                    always_cc.discard(sid)
                    always_cc.add(addr)
                elif notify_sys.use_public_cc:
                    always_cc.add(addr)
            else:
                self.log.debug(
                    "%s was unable to use an address '%s' for '%s "
                    "[%s]'", self.__class__.__name__, addr, sid, auth)

        outputs = {}
        failed = []
        for fmt, formatter in formats.iteritems():
            if fmt not in addresses and fmt != 'text/plain':
                continue
            try:
                outputs[fmt] = formatter.format(transport, fmt, event)
            except Exception as e:
                self.log.warning(
                    '%s caught exception while '
                    'formatting %s to %s for %s: %s%s',
                    self.__class__.__name__, event.realm, fmt, transport,
                    formatter.__class__, exception_to_unicode(e,
                                                              traceback=True))
                failed.append(fmt)

        # Fallback to text/plain when formatter is broken
        if failed and 'text/plain' in outputs:
            for fmt in failed:
                addresses.setdefault('text/plain', set()) \
                         .update(addresses.pop(fmt, ()))

        for fmt, addrs in addresses.iteritems():
            self.log.debug("%s is sending event as '%s' to: %s",
                           self.__class__.__name__, fmt, ', '.join(addrs))
            message = self._create_message(fmt, outputs)
            if message:
                addrs = set(addrs)
                cc_addrs = sorted(addrs & always_cc)
                bcc_addrs = sorted(addrs - always_cc)
                self._do_send(transport, event, message, cc_addrs, bcc_addrs)
            else:
                self.log.warning("%s cannot send event '%s' as '%s': %s",
                                 self.__class__.__name__, event.realm, fmt,
                                 ', '.join(addrs))

    def _create_message(self, format, outputs):
        if format not in outputs:
            return None
        message = create_mime_multipart('related')
        maintype, subtype = format.split('/')
        preferred = create_mime_text(outputs[format], subtype, self._charset)
        if format != 'text/plain' and 'text/plain' in outputs:
            alternative = create_mime_multipart('alternative')
            alternative.attach(
                create_mime_text(outputs['text/plain'], 'plain',
                                 self._charset))
            alternative.attach(preferred)
            preferred = alternative
        message.attach(preferred)
        return message

    def _do_send(self, transport, event, message, cc_addrs, bcc_addrs):
        notify_sys = NotificationSystem(self.env)
        smtp_from = notify_sys.smtp_from
        smtp_from_name = notify_sys.smtp_from_name or self.env.project_name
        smtp_replyto = notify_sys.smtp_replyto
        if not notify_sys.use_short_addr and notify_sys.smtp_default_domain:
            if smtp_from and '@' not in smtp_from:
                smtp_from = '%s@%s' % (smtp_from,
                                       notify_sys.smtp_default_domain)
            if smtp_replyto and '@' not in smtp_replyto:
                smtp_replyto = '%s@%s' % (smtp_replyto,
                                          notify_sys.smtp_default_domain)

        headers = {}
        headers['X-Mailer'] = 'Trac %s, by Edgewall Software'\
                              % self.env.trac_version
        headers['X-Trac-Version'] = self.env.trac_version
        headers['X-Trac-Project'] = self.env.project_name
        headers['X-URL'] = self.env.project_url
        headers['X-Trac-Realm'] = event.realm
        headers['Precedence'] = 'bulk'
        headers['Auto-Submitted'] = 'auto-generated'
        if isinstance(event.target, (list, tuple)):
            targetid = ','.join(map(get_target_id, event.target))
        else:
            targetid = get_target_id(event.target)
        rootid = create_message_id(self.env,
                                   targetid,
                                   smtp_from,
                                   None,
                                   more=event.realm)
        if event.category == 'created':
            headers['Message-ID'] = rootid
        else:
            headers['Message-ID'] = create_message_id(self.env,
                                                      targetid,
                                                      smtp_from,
                                                      event.time,
                                                      more=event.realm)
            headers['In-Reply-To'] = rootid
            headers['References'] = rootid
        headers['Date'] = formatdate()
        headers['From'] = (smtp_from_name, smtp_from) \
                          if smtp_from_name else smtp_from
        headers['To'] = 'undisclosed-recipients: ;'
        if cc_addrs:
            headers['Cc'] = ', '.join(cc_addrs)
        if bcc_addrs:
            headers['Bcc'] = ', '.join(bcc_addrs)
        headers['Reply-To'] = smtp_replyto

        for k, v in headers.iteritems():
            set_header(message, k, v, self._charset)
        for decorator in self.decorators:
            decorator.decorate_message(event, message, self._charset)

        from_name, from_addr = parseaddr(str(message['From']))
        to_addrs = set()
        for name in ('To', 'Cc', 'Bcc'):
            values = map(str, message.get_all(name, ()))
            to_addrs.update(addr for name, addr in getaddresses(values)
                            if addr)
        del message['Bcc']
        notify_sys.send_email(from_addr, list(to_addrs), message.as_string())
Ejemplo n.º 7
0
class TracPasswordStoreUser(Component):
    tracpasswordstore_observers = ExtensionPoint(IPasswordStore)

    @tracob_first
    def has_user(self, *_args, **_kw):
        return self.tracpasswordstore_observers
Ejemplo n.º 8
0
 def __init__(self, section, name, interface, default=None, doc='',
              doc_domain='tracini', doc_args=None):
     Option.__init__(self, section, name, default, doc, doc_domain,
                     doc_args)
     self.xtnpt = ExtensionPoint(interface)
Ejemplo n.º 9
0
class PermissionsAdminPanel(Component):
    implements(IAdminPanelProvider)

    # Extension points
    project_change_listeners = ExtensionPoint(IProjectChangeListener)

    # list in order in which they should be listed in the UI
    MEMBER_TYPES = ('login_status', 'user', 'organization', 'ldap')

    def __init__(self):
        self.conf = Configuration.instance()

    # IAdminPanelProvider methods
    def get_admin_panels(self, req):
        if 'PERMISSION_GRANT' in req.perm or 'PERMISSION_REVOKE' in req.perm:
            yield ('general', _('General'), 'permissions', _('Permissions'))

    def render_admin_panel(self, req, cat, page, path_info):

        add_script(req, 'multiproject/js/jquery-ui.js')
        add_script(req, 'multiproject/js/permissions.js')
        add_stylesheet(req, 'multiproject/css/jquery-ui.css')
        add_stylesheet(req, 'multiproject/css/permissions.css')

        project = Project.get(self.env)  #
        is_normal_project = self.env.project_identifier != \
                            self.env.config.get('multiproject', 'sys_home_project_name')

        # API instances
        perm_sys = PermissionSystem(self.env)
        group_store = CQDEUserGroupStore(env=self.env)
        org_store = CQDEOrganizationStore.instance()
        if is_normal_project:
            membership = MembershipApi(self.env, Project.get(self.env))
        else:
            membership = None

        if req.method == 'POST':
            action = req.args.get('action')
            if action == 'remove_member':
                self._remove_member(req, group_store)
            elif action == 'add_member':
                add_type = req.args.get('add_type')
                if add_type == 'user':
                    self._add_user(req, group_store, membership)
                elif add_type == 'organization':
                    self._add_organization(req, group_store)
                elif add_type == 'ldap_group':
                    self._add_ldap_group(req, group_store)
                elif add_type == 'login_status':
                    login_status = req.args.get('login_status')
                    if login_status not in ('authenticated', 'anonymous'):
                        raise TracError('Invalid arguments')
                    self._add_user(req,
                                   group_store,
                                   membership,
                                   username=login_status)
                else:
                    raise TracError('Invalid add_type')
            elif action == 'add_permission':
                self._add_perm_to_group(req, group_store, perm_sys)
            elif action == 'remove_permission':
                self._remove_permission(req, group_store, perm_sys)
            elif action == 'create_group':
                self._create_group(req, group_store, perm_sys)
            elif action == 'remove_group':
                self._remove_group(req, group_store)
            elif action == 'add_organization':
                self._add_organization(req, group_store)
            elif action == 'decline_membership':
                self._decline_membership(req, membership)
            elif 'makepublic' in req.args:
                project_api = Projects()
                if conf.allow_public_projects:
                    self._make_public(req, project)
                    project_api.add_public_project_visibility(project.id)
                    # Reload page
                    return req.redirect(req.href(req.path_info))
                else:
                    raise TracError("Public projects are disabled", "Error!")
            elif 'makeprivate' in req.args:
                project_api = Projects()
                self._make_private(req, project)
                project_api.remove_public_project_visibility(project.id)
                # Reload page
                return req.redirect(req.href(req.path_info))
            else:
                raise TracError('Unknown action %s' % action)

        # get membership request list after form posts have been processed
        if is_normal_project:
            membership_requests = set(membership.get_membership_requests())
        else:
            membership_requests = set()

        permissions = set(perm_sys.get_actions())

        # check if project if current configuration and permission state is in such state that
        # permission editions are likely fail
        invalid_state = None

        if is_normal_project:
            is_a_public = project.public
        else:
            is_a_public = ""

        try:
            group_store.is_valid_group_members()
        except InvalidPermissionsState, e:
            add_warning(
                req,
                _('Application permission configuration conflicts with project permissions. '
                  'Before you can fully edit permissions or users you will need to either remove '
                  'offending permissions or set correct application configuration. Page reload'
                  'is required to update this warning.'))
            add_warning(req, e.message)

        return 'permissions.html', {
            'perm_data':
            self._perm_data(group_store, perm_sys),
            'theme_htdocs_location':
            self.env.config.get('multiproject', 'theme_htdocs_location',
                                '/htdocs/theme'),
            'permissions':
            sorted(permissions),
            'organizations':
            sorted([org.name for org in org_store.get_organizations()]),
            'use_organizations':
            self.config.getbool('multiproject-users', 'use_organizations',
                                False),
            'use_ldap':
            self.config.getbool('multiproject', 'ldap_groups_enabled', False),
            'membership_requests':
            membership_requests,
            'invalid_state':
            invalid_state,
            'is_public':
            is_a_public,
            'allow_public_projects':
            conf.allow_public_projects
        }
Ejemplo n.º 10
0
class UserProfilesListMacro(WikiMacroBase):
    """Returns project's team roster.

Usage:

{{{
# Without arguments returns current active user profiles (with enabled='1')
[[UserProfilesList]]
# Returns all userProfiles with role='developer' and enabled='1'
[[UserProfilesList(role='developer', enabled='1')]]
# Returns all userProfiles with name like 'someName'
[[UserProfilesList(name='%someName%')]]
# Returns cbalan's profile and user profiles with role='%arh%'
[[UserProfilesList({id='cbalan'},{role='%arh%'})]]
# Adds style and class attributes to box layout
[[UserProfilesList(|class=someCSS_Class, style=border:1px solid green;padding:12px)]]
}}}
    """
    cells_providers = ExtensionPoint(IUserProfilesListMacroCellContributor)

    def expand_macro(self, formatter, name, content):

        env = self.env
        req = formatter.req

        content_args = {}
        data = dict(user_profiles=[], user_profile_fields={})
        layout_args = {}
        rendered_result = ""
        user_profile_templates = []

        # collecting arguments
        if content:
            for i, macro_args in enumerate(content.split('|')):
                if i == 0:
                    content_args = MacroArguments(macro_args)
                    continue
                if i == 1:
                    layout_args = MacroArguments(macro_args)
                    break

            # extracting userProfile attrs
            if len(content_args) > 0:
                user_profile_templates.append(User(**content_args))

            if len(content_args.get_list_args()) > 0:
                for list_item in content_args.get_list_args():
                    user_profile_templates.append(
                        User(**MacroArguments(list_item[1:len(list_item) -
                                                        1])))

        # adding profiles fields description
        data['user_profile_fields'].update(
            UserProfileManager(env).get_user_profile_fields(
                ignore_internal=True))

        # removing picture_href
        data['user_profile_fields'].pop('picture_href')

        def inline_wiki_to_html(text):
            return wiki_to_html(text, env, req)

        data['wiki_to_html'] = inline_wiki_to_html

        # grabbing users
        if len(user_profile_templates) > 0:
            data['user_profiles'] = UserManager(env).search_users(
                user_profile_templates)
        else:
            data['user_profiles'] = UserManager(env).get_active_users()

        data['cells'] = list(self._get_cells(req, data['user_profiles']))

        # add stylesheet&script
        add_script(req, 'tracusermanager/js/macros_um_profile.js')
        add_stylesheet(req, 'tracusermanager/css/macros_um_profile.css')

        # render template
        template = Chrome(env).load_template('macro_um_profile.html',
                                             method='xhtml')
        data = Chrome(env).populate_data(req, {'users': data})

        rendered_result = template.generate(**data)

        # wrap everything
        if len(layout_args) > 0:
            rendered_result = html.div(rendered_result, **layout_args)

        return rendered_result

    def _get_cells(self, req, user_list):
        for provider in self.cells_providers:
            for cell, label, order in provider.get_userlistmacro_cells():
                if label == 'Email':
                    cell = Chrome(self.env).format_author(req, cell)
                yield dict(name=cell,
                           label=label,
                           order=order,
                           render_method=provider.render_userlistmacro_cell)
Ejemplo n.º 11
0
class PrivateReports(Component):
    group_providers = ExtensionPoint(IPermissionGroupProvider)

    implements(ITemplateStreamFilter, IEnvironmentSetupParticipant,
               IAdminPanelProvider, ITemplateProvider, IRequestFilter,
               IPermissionRequestor)

    ### IRequestFilter methods

    def pre_process_request(self, req, handler):
        if not isinstance(handler, ReportModule):
            return handler
        report_id = req.args.get('id')
        if not report_id or self._has_permission(req.authname, report_id):
            return handler
        else:
            self.log.debug("User %s doesn't have permission to view report %s "
                           % (req.authname, report_id))
            raise TracError("You don't have permission to access this report",
                            "No Permission")

    def post_process_request(self, req, template, data, content_type):
        return template, data, content_type

    ### ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    ### IAdminPanelProvider methods

    def get_admin_panels(self, req):
        if req.perm.has_permission('TRAC_ADMIN'):
            yield ('reports', 'Reports',
                   'privatereports', 'Private Reports')

    def render_admin_panel(self, req, cat, page, path_info):
        if page == 'privatereports':
            reports = self._get_reports()
            data = {
                'reports': reports
            }
            if req.method == 'POST':
                report_id = req.args.get('report_id')
                try:
                    report_id = int(report_id)
                except ValueError:
                    req.redirect(self.env.href.admin.reports('privatereports'))
                if req.args.get('add'):
                    new_permission = req.args.get('newpermission')
                    if new_permission is None or \
                            new_permission.isupper() is False:
                        req.redirect(
                            self.env.href.admin.reports('privatereports'))
                    self._insert_report_permission(report_id, new_permission)
                    data['report_permissions'] = \
                        self._get_report_permissions(report_id) or ''
                    data['show_report'] = report_id
                elif req.args.get('remove'):
                    arg_report_permissions = req.args.get('report_permissions')
                    if arg_report_permissions is None:
                        req.redirect(
                            self.env.href.admin.reports('privatereports'))
                    report_permissions = \
                        self._get_report_permissions(report_id)
                    report_permissions = set(report_permissions)
                    to_remove = set()
                    if type(arg_report_permissions) in StringTypes:
                        to_remove.update([arg_report_permissions])
                    elif type(arg_report_permissions) == ListType:
                        to_remove.update(arg_report_permissions)
                    else:
                        req.redirect(
                            self.env.href.admin.reports('privatereports'))
                    report_permissions = report_permissions - to_remove
                    self._alter_report_permissions(report_id,
                                                   report_permissions)
                    data['report_permissions'] = report_permissions or ''
                    data['show_report'] = report_id
                elif req.args.get('show'):
                    report_permissions = \
                        self._get_report_permissions(report_id)
                    data['report_permissions'] = report_permissions or ''
                    data['show_report'] = report_id
            else:
                report_permissions = \
                    self._get_report_permissions(reports[0][1])
                data['report_permissions'] = report_permissions or ''
            return 'admin_privatereports.html', data

    ### IEnvironmentSetupParticipant methods

    def environment_created(self):
        db = self.env.get_db_cnx()
        if self.environment_needs_upgrade(db):
            self.upgrade_environment(db)

    def environment_needs_upgrade(self, db):
        cursor = db.cursor()
        try:
            cursor.execute("SELECT report_id, permission FROM private_report")
            return False
        except:
            return True

    def upgrade_environment(self, db):
        cursor = db.cursor()
        try:
            cursor.execute("DROP TABLE IF EXISTS private_report")
            db.commit()
        except:
            cursor.connection.rollback()
        try:
            cursor = db.cursor()
            cursor.execute("""
                CREATE TABLE private_report(report_id integer,
                  permission text)""")
            db.commit()
        except:
            cursor.connection.rollback()

    ### ITemplateStreamFilter methods

    def filter_stream(self, req, method, filename, stream, data):
        if not filename == 'report_list.html':
            return stream
        stream_buffer = StreamBuffer()

        from pkg_resources import parse_version
        if parse_version(trac_version) < parse_version('1.0'):
            delimiter = '</tr>'
            selector = '//tbody/tr'
        else:
            delimiter = '</div>'
            selector = '//div[@class="reports"]/div'

        def check_report_permission():
            report_stream = str(stream_buffer)
            reports_raw = report_stream.split(delimiter)
            report_stream = ''
            for row in reports_raw:
                if row is not None and len(row) != 0 and 'View report' in row:
                    # determine the report id
                    s = row.find('/report/')
                    if s == -1:
                        continue
                    e = row.find('\"', s)
                    if e == -1:
                        continue
                    report_id = row[s+len('/report/'):e]
                    if self._has_permission(req.authname, report_id):
                        report_stream += row
                    else:
                        self.log.debug("Removing report %s from list because "
                                       "%s doesn't have permission to view" %
                                       (report_id, req.authname))
                elif 'View report' in row:
                    report_stream += row
            return HTML(report_stream)
        return stream | Transformer(selector) \
            .copy(stream_buffer) \
            .replace(check_report_permission)

    ### IPermissionRequestor methods

    def get_permission_actions(self):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT permission FROM private_report GROUP BY permission""")
        report_perms = []
        try:
            for permission in cursor.fetchall():
                report_perms.append(permission[0])
        except:
            pass
        return tuple(report_perms)

    ### Internal methods

    def _has_permission(self, user, report_id):
        report_permissions = self._get_report_permissions(report_id)
        if not report_permissions:
            return True
        perms = PermissionSystem(self.env)
        report_permissions = set(report_permissions)
        user_perm = set(perms.get_user_permissions(user))
        groups = set(self._get_user_groups(user))
        user_perm.update(groups)
        if report_permissions.intersection(user_perm) != set([]):
            return True
        return False

    def _get_user_groups(self, user):
        subjects = set([user])
        for provider in self.group_providers:
            subjects.update(provider.get_permission_groups(user) or [])
        groups = []
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT action FROM permission WHERE username = %s""", (user,))
        rows = cursor.fetchall()
        for action in rows:
            if action[0].isupper():
                groups.append(action[0])
        return groups

    def _get_reports(self):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("SELECT title, id FROM report")
        reports = cursor.fetchall()
        return reports

    def _insert_report_permission(self, report_id, permission):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            INSERT INTO private_report(report_id, permission)
            VALUES(%s, %s)""", (int(report_id), str(permission)))
        db.commit()

    def _alter_report_permissions(self, report_id, permissions):
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            DELETE FROM private_report WHERE report_id=%s
            """, (int(report_id),))
        db.commit()
        for permission in permissions:
            self._insert_report_permission(report_id, permission)

    def _get_report_permissions(self, report_id):
        report_perms = []
        db = self.env.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("""
            SELECT permission FROM private_report
            WHERE report_id=%s GROUP BY permission""", (int(report_id),))
        for perm in cursor.fetchall():
            report_perms.append(perm[0])
        return report_perms
Ejemplo n.º 12
0
class SessionAdmin(Component):
    """trac-admin command provider for session management"""

    implements(IAdminCommandProvider)

    request_handlers = ExtensionPoint(IRequestHandler)

    def get_admin_commands(self):
        hints = {
            'datetime': get_datetime_format_hint(get_console_locale(self.env)),
            'iso8601': get_datetime_format_hint('iso8601'),
        }
        yield ('session list', '[sid[:0|1]] [...]',
               """List the name and email for the given sids

               Specifying the sid 'anonymous' lists all unauthenticated
               sessions, and 'authenticated' all authenticated sessions.
               '*' lists all sessions, and is the default if no sids are
               given.

               An sid suffix ':0' operates on an unauthenticated session with
               the given sid, and a suffix ':1' on an authenticated session
               (the default).""", self._complete_list, self._do_list)

        yield ('session add', '<sid[:0|1]> [name] [email]',
               """Create a session for the given sid

               Populates the name and email attributes for the given session.
               Adding a suffix ':0' to the sid makes the session
               unauthenticated, and a suffix ':1' makes it authenticated (the
               default if no suffix is specified).""", None, self._do_add)

        yield ('session set', '<name|email|default_handler> '
               '<sid[:0|1]> <value>',
               """Set the name or email attribute of the given sid

               An sid suffix ':0' operates on an unauthenticated session with
               the given sid, and a suffix ':1' on an authenticated session
               (the default).""", self._complete_set, self._do_set)

        yield ('session delete', '<sid[:0|1]> [...]',
               """Delete the session of the specified sid

               An sid suffix ':0' operates on an unauthenticated session with
               the given sid, and a suffix ':1' on an authenticated session
               (the default). Specifying the sid 'anonymous' will delete all
               anonymous sessions.""", self._complete_delete, self._do_delete)

        yield ('session purge', '<age>',
               """Purge anonymous sessions older than the given age or date

               Age may be specified as a relative time like "90 days ago", or
               as a date in the "%(datetime)s" or "%(iso8601)s" (ISO 8601)
               format.""" % hints, None, self._do_purge)

    @lazy
    def _valid_default_handlers(self):
        return sorted(handler.__class__.__name__
                      for handler in self.request_handlers
                      if is_valid_default_handler(handler))

    def _split_sid(self, sid):
        if sid.endswith(':0'):
            return sid[:-2], 0
        elif sid.endswith(':1'):
            return sid[:-2], 1
        else:
            return sid, 1

    def _get_sids(self):
        rows = self.env.db_query("SELECT sid, authenticated FROM session")
        return ['%s:%d' % (sid, auth) for sid, auth in rows]

    def _get_list(self, sids):
        all_anon = 'anonymous' in sids or '*' in sids
        all_auth = 'authenticated' in sids or '*' in sids
        sids = set(
            self._split_sid(sid) for sid in sids
            if sid not in ('anonymous', 'authenticated', '*'))
        rows = self.env.db_query("""
            SELECT DISTINCT s.sid, s.authenticated, s.last_visit,
                            n.value, e.value, h.value
            FROM session AS s
              LEFT JOIN session_attribute AS n
                ON (n.sid=s.sid AND n.authenticated=s.authenticated
                    AND n.name='name')
              LEFT JOIN session_attribute AS e
                ON (e.sid=s.sid AND e.authenticated=s.authenticated
                    AND e.name='email')
              LEFT JOIN session_attribute AS h
                ON (h.sid=s.sid AND h.authenticated=s.authenticated
                    AND h.name='default_handler')
            ORDER BY s.sid, s.authenticated
            """)
        for sid, authenticated, last_visit, name, email, handler in rows:
            if all_anon and not authenticated or all_auth and authenticated \
                    or (sid, authenticated) in sids:
                yield (sid, authenticated,
                       format_date(to_datetime(last_visit),
                                   console_date_format), name, email, handler)

    def _complete_list(self, args):
        all_sids = self._get_sids() + ['*', 'anonymous', 'authenticated']
        return set(all_sids) - set(args)

    def _complete_set(self, args):
        if len(args) == 1:
            return ['name', 'email']
        elif len(args) == 2:
            return self._get_sids()

    def _complete_delete(self, args):
        all_sids = self._get_sids() + ['anonymous']
        return set(all_sids) - set(args)

    def _do_list(self, *sids):
        if not sids:
            sids = ['*']
        headers = (_("SID"), _("Auth"), _("Last Visit"), _("Name"), _("Email"),
                   _("Default Handler"))
        print_table(self._get_list(sids), headers)

    def _do_add(self, sid, name=None, email=None):
        sid, authenticated = self._split_sid(sid)
        with self.env.db_transaction as db:
            try:
                db("INSERT INTO session VALUES (%s, %s, %s)",
                   (sid, authenticated, int(time.time())))
            except Exception:
                raise AdminCommandError(
                    _("Session '%(sid)s' already exists", sid=sid))
            if name is not None:
                db("INSERT INTO session_attribute VALUES (%s,%s,'name',%s)",
                   (sid, authenticated, name))
            if email is not None:
                db("INSERT INTO session_attribute VALUES (%s,%s,'email',%s)",
                   (sid, authenticated, email))
        self.env.invalidate_known_users_cache()

    def _do_set(self, attr, sid, val):
        if attr not in ('name', 'email', 'default_handler'):
            raise AdminCommandError(
                _("Invalid attribute '%(attr)s'", attr=attr))
        if attr == 'default_handler':
            if val and val not in self._valid_default_handlers:
                raise AdminCommandError(
                    _("Invalid default_handler '%(val)s'", val=val))
        sid, authenticated = self._split_sid(sid)
        with self.env.db_transaction as db:
            if not db(
                    """SELECT sid FROM session
                         WHERE sid=%s AND authenticated=%s""",
                (sid, authenticated)):
                raise AdminCommandError(
                    _("Session '%(sid)s' not found", sid=sid))
            db(
                """
                DELETE FROM session_attribute
                WHERE sid=%s AND authenticated=%s AND name=%s
                """, (sid, authenticated, attr))
            db("INSERT INTO session_attribute VALUES (%s, %s, %s, %s)",
               (sid, authenticated, attr, val))
        self.env.invalidate_known_users_cache()

    def _do_delete(self, *sids):
        with self.env.db_transaction as db:
            for sid in sids:
                sid, authenticated = self._split_sid(sid)
                if sid == 'anonymous':
                    db("DELETE FROM session WHERE authenticated=0")
                    db("DELETE FROM session_attribute WHERE authenticated=0")
                else:
                    db(
                        """
                        DELETE FROM session
                        WHERE sid=%s AND authenticated=%s
                        """, (sid, authenticated))
                    db(
                        """
                        DELETE FROM session_attribute
                        WHERE sid=%s AND authenticated=%s
                        """, (sid, authenticated))
        self.env.invalidate_known_users_cache()

    def _do_purge(self, age):
        when = parse_date(age,
                          hint='datetime',
                          locale=get_console_locale(self.env))
        with self.env.db_transaction as db:
            ts = to_timestamp(when)
            db(
                """
                DELETE FROM session
                WHERE authenticated=0 AND last_visit<%s
                """, (ts, ))
            db("""
                DELETE FROM session_attribute
                WHERE authenticated=0
                      AND sid NOT IN (SELECT sid FROM session
                                      WHERE authenticated=0)
                """)
Ejemplo n.º 13
0
class Environment(Component, ComponentManager):
    """Trac environment manager.

    Trac stores project information in a Trac environment. It consists
    of a directory structure containing among other things:

    * a configuration file,
    * project-specific templates and plugins,
    * the wiki and ticket attachments files,
    * the SQLite database file (stores tickets, wiki pages...)
      in case the database backend is sqlite

    """

    implements(ISystemInfoProvider)

    required = True

    system_info_providers = ExtensionPoint(ISystemInfoProvider)
    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)

    components_section = ConfigSection(
        'components', """This section is used to enable or disable components
        provided by plugins, as well as by Trac itself. The component
        to enable/disable is specified via the name of the
        option. Whether its enabled is determined by the option value;
        setting the value to `enabled` or `on` will enable the
        component, any other value (typically `disabled` or `off`)
        will disable the component.

        The option name is either the fully qualified name of the
        components or the module/package prefix of the component. The
        former enables/disables a specific component, while the latter
        enables/disables any component in the specified
        package/module.

        Consider the following configuration snippet:
        {{{
        [components]
        trac.ticket.report.ReportModule = disabled
        acct_mgr.* = enabled
        }}}

        The first option tells Trac to disable the
        [wiki:TracReports report module].
        The second option instructs Trac to enable all components in
        the `acct_mgr` package. Note that the trailing wildcard is
        required for module/package matching.

        To view the list of active components, go to the ''Plugins''
        page on ''About Trac'' (requires `CONFIG_VIEW`
        [wiki:TracPermissions permissions]).

        See also: TracPlugins
        """)

    shared_plugins_dir = PathOption(
        'inherit', 'plugins_dir', '',
        """Path to the //shared plugins directory//.

        Plugins in that directory are loaded in addition to those in
        the directory of the environment `plugins`, with this one
        taking precedence.
        """)

    base_url = Option(
        'trac', 'base_url', '', """Reference URL for the Trac deployment.

        This is the base URL that will be used when producing
        documents that will be used outside of the web browsing
        context, like for example when inserting URLs pointing to Trac
        resources in notification e-mails.""")

    base_url_for_redirect = BoolOption(
        'trac', 'use_base_url_for_redirect', False,
        """Optionally use `[trac] base_url` for redirects.

        In some configurations, usually involving running Trac behind
        a HTTP proxy, Trac can't automatically reconstruct the URL
        that is used to access it. You may need to use this option to
        force Trac to use the `base_url` setting also for
        redirects. This introduces the obvious limitation that this
        environment will only be usable when accessible from that URL,
        as redirects are frequently used.
        """)

    secure_cookies = BoolOption(
        'trac', 'secure_cookies', False,
        """Restrict cookies to HTTPS connections.

        When true, set the `secure` flag on all cookies so that they
        are only sent to the server on HTTPS connections. Use this if
        your Trac instance is only accessible through HTTPS.
        """)

    project_name = Option('project', 'name', 'My Project',
                          """Name of the project.""")

    project_description = Option('project', 'descr', 'My example project',
                                 """Short description of the project.""")

    project_url = Option(
        'project', 'url', '',
        """URL of the main project web site, usually the website in
        which the `base_url` resides. This is used in notification
        e-mails.""")

    project_admin = Option(
        'project', 'admin', '',
        """E-Mail address of the project's administrator.""")

    project_admin_trac_url = Option(
        'project', 'admin_trac_url', '.',
        """Base URL of a Trac instance where errors in this Trac
        should be reported.

        This can be an absolute or relative URL, or '.' to reference
        this Trac instance. An empty value will disable the reporting
        buttons.
        """)

    project_footer = Option(
        'project', 'footer',
        N_('Visit the Trac open source project at<br />'
           '<a href="http://trac.edgewall.org/">'
           'http://trac.edgewall.org/</a>'),
        """Page footer text (right-aligned).""")

    project_icon = Option('project', 'icon', 'common/trac.ico',
                          """URL of the icon of the project.""")

    log_type = ChoiceOption('logging',
                            'log_type',
                            log.LOG_TYPES + log.LOG_TYPE_ALIASES,
                            """Logging facility to use.

        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""",
                            case_sensitive=False)

    log_file = Option(
        'logging', 'log_file', 'trac.log',
        """If `log_type` is `file`, this should be a path to the
        log-file.  Relative paths are resolved relative to the `log`
        directory of the environment.""")

    log_level = ChoiceOption('logging',
                             'log_level',
                             tuple(reversed(log.LOG_LEVELS)) +
                             log.LOG_LEVEL_ALIASES,
                             """Level of verbosity in log.

        Should be one of (`CRITICAL`, `ERROR`, `WARNING`, `INFO`, `DEBUG`).
        """,
                             case_sensitive=False)

    log_format = Option(
        'logging', 'log_format', None, """Custom logging format.

        If nothing is set, the following will be used:

        `Trac[$(module)s] $(levelname)s: $(message)s`

        In addition to regular key names supported by the
        [http://docs.python.org/library/logging.html Python logger library]
        one could use:

        - `$(path)s`     the path for the current environment
        - `$(basename)s` the last path component of the current environment
        - `$(project)s`  the project name

        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
        would be interpreted by the !ConfigParser itself.

        Example:
        `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`
        """)

    def __init__(self, path, create=False, options=[]):
        """Initialize the Trac environment.

        :param path:   the absolute path to the Trac environment
        :param create: if `True`, the environment is created and
                       populated with default data; otherwise, the
                       environment is expected to already exist.
        :param options: A list of `(section, name, value)` tuples that
                        define configuration options
        """
        ComponentManager.__init__(self)

        self.path = os.path.normpath(os.path.normcase(path))
        self.log = None
        self.config = None

        if create:
            self.create(options)
            for setup_participant in self.setup_participants:
                setup_participant.environment_created()
        else:
            self.verify()
            self.setup_config()

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.path)

    @lazy
    def name(self):
        """The environment name.

        :since: 1.2
        """
        return os.path.basename(self.path)

    @property
    def env(self):
        """Property returning the `Environment` object, which is often
        required for functions and methods that take a `Component` instance.
        """
        # The cached decorator requires the object have an `env` attribute.
        return self

    @property
    def system_info(self):
        """List of `(name, version)` tuples describing the name and
        version information of external packages used by Trac and plugins.
        """
        info = []
        for provider in self.system_info_providers:
            info.extend(provider.get_system_info() or [])
        return sorted(set(info),
                      key=lambda args: (args[0] != 'Trac', args[0].lower()))

    def get_systeminfo(self):
        """Return a list of `(name, version)` tuples describing the name
        and version information of external packages used by Trac and plugins.

        :since 1.3.1: deprecated and will be removed in 1.5.1. Use
                      system_info property instead.
        """
        return self.system_info

    # ISystemInfoProvider methods

    def get_system_info(self):
        yield 'Trac', self.trac_version
        yield 'Python', sys.version
        yield 'setuptools', setuptools.__version__
        if pytz is not None:
            yield 'pytz', pytz.__version__
        if hasattr(self, 'webfrontend_version'):
            yield self.webfrontend, self.webfrontend_version

    def component_activated(self, component):
        """Initialize additional member variables for components.

        Every component activated through the `Environment` object
        gets three member variables: `env` (the environment object),
        `config` (the environment configuration) and `log` (a logger
        object)."""
        component.env = self
        component.config = self.config
        component.log = self.log

    def _component_name(self, name_or_class):
        name = name_or_class
        if not isinstance(name_or_class, basestring):
            name = name_or_class.__module__ + '.' + name_or_class.__name__
        return name.lower()

    @lazy
    def _component_rules(self):
        _rules = {}
        for name, value in self.components_section.options():
            name = name.rstrip('.*').lower()
            _rules[name] = as_bool(value)
        return _rules

    def is_component_enabled(self, cls):
        """Implemented to only allow activation of components that are
        not disabled in the configuration.

        This is called by the `ComponentManager` base class when a
        component is about to be activated. If this method returns
        `False`, the component does not get activated. If it returns
        `None`, the component only gets activated if it is located in
        the `plugins` directory of the environment.
        """
        component_name = self._component_name(cls)

        rules = self._component_rules
        cname = component_name
        while cname:
            enabled = rules.get(cname)
            if enabled is not None:
                return enabled
            idx = cname.rfind('.')
            if idx < 0:
                break
            cname = cname[:idx]

        # By default, all components in the trac package except
        # in trac.test or trac.tests are enabled
        return component_name.startswith('trac.') and \
               not component_name.startswith('trac.test.') and \
               not component_name.startswith('trac.tests.') or None

    def enable_component(self, cls):
        """Enable a component or module."""
        self._component_rules[self._component_name(cls)] = True
        super(Environment, self).enable_component(cls)

    @contextmanager
    def component_guard(self, component, reraise=False):
        """Traps any runtime exception raised when working with a component
        and logs the error.

        :param component: the component responsible for any error that
                          could happen inside the context
        :param reraise: if `True`, an error is logged but not suppressed.
                        By default, errors are suppressed.

        """
        try:
            yield
        except TracError as e:
            self.log.warning("Component %s failed with %s", component,
                             exception_to_unicode(e))
            if reraise:
                raise
        except Exception as e:
            self.log.error("Component %s failed with %s", component,
                           exception_to_unicode(e, traceback=True))
            if reraise:
                raise

    def verify(self):
        """Verify that the provided path points to a valid Trac environment
        directory."""
        try:
            tag = read_file(os.path.join(self.path, 'VERSION')).splitlines()[0]
            if tag != _VERSION:
                raise Exception(
                    _("Unknown Trac environment type '%(type)s'", type=tag))
        except Exception as e:
            raise TracError(
                _("No Trac environment found at %(path)s\n"
                  "%(e)s",
                  path=self.path,
                  e=e))

    @lazy
    def db_exc(self):
        """Return an object (typically a module) containing all the
        backend-specific exception types as attributes, named
        according to the Python Database API
        (http://www.python.org/dev/peps/pep-0249/).

        To catch a database exception, use the following pattern::

            try:
                with env.db_transaction as db:
                    ...
            except env.db_exc.IntegrityError as e:
                ...
        """
        return DatabaseManager(self).get_exceptions()

    @property
    def db_query(self):
        """Return a context manager
        (`~trac.db.api.QueryContextManager`) which can be used to
        obtain a read-only database connection.

        Example::

            with env.db_query as db:
                cursor = db.cursor()
                cursor.execute("SELECT ...")
                for row in cursor.fetchall():
                    ...

        Note that a connection retrieved this way can be "called"
        directly in order to execute a query::

            with env.db_query as db:
                for row in db("SELECT ..."):
                    ...

        :warning: after a `with env.db_query as db` block, though the
          `db` variable is still defined, you shouldn't use it as it
          might have been closed when exiting the context, if this
          context was the outermost context (`db_query` or
          `db_transaction`).

        If you don't need to manipulate the connection itself, this
        can even be simplified to::

            for row in env.db_query("SELECT ..."):
                ...

        """
        return QueryContextManager(self)

    @property
    def db_transaction(self):
        """Return a context manager
        (`~trac.db.api.TransactionContextManager`) which can be used
        to obtain a writable database connection.

        Example::

            with env.db_transaction as db:
                cursor = db.cursor()
                cursor.execute("UPDATE ...")

        Upon successful exit of the context, the context manager will
        commit the transaction. In case of nested contexts, only the
        outermost context performs a commit. However, should an
        exception happen, any context manager will perform a rollback.
        You should *not* call `commit()` yourself within such block,
        as this will force a commit even if that transaction is part
        of a larger transaction.

        Like for its read-only counterpart, you can directly execute a
        DML query on the `db`::

            with env.db_transaction as db:
                db("UPDATE ...")

        :warning: after a `with env.db_transaction` as db` block,
          though the `db` variable is still available, you shouldn't
          use it as it might have been closed when exiting the
          context, if this context was the outermost context
          (`db_query` or `db_transaction`).

        If you don't need to manipulate the connection itself, this
        can also be simplified to::

            env.db_transaction("UPDATE ...")

        """
        return TransactionContextManager(self)

    def shutdown(self, tid=None):
        """Close the environment."""
        from trac.versioncontrol.api import RepositoryManager
        RepositoryManager(self).shutdown(tid)
        DatabaseManager(self).shutdown(tid)
        if tid is None:
            log.shutdown(self.log)

    def create(self, options=[]):
        """Create the basic directory structure of the environment,
        initialize the database and populate the configuration file
        with default values.

        If options contains ('inherit', 'file'), default values will
        not be loaded; they are expected to be provided by that file
        or other options.

        :raises TracError: if the base directory of `path` does not exist.
        :raises TracError: if `path` exists and is not empty.
        """
        base_dir = os.path.dirname(self.path)
        if not os.path.exists(base_dir):
            raise TracError(
                _(
                    "Base directory '%(env)s' does not exist. Please create it "
                    "and retry.",
                    env=base_dir))

        if os.path.exists(self.path) and os.listdir(self.path):
            raise TracError(_("Directory exists and is not empty."))

        # Create the directory structure
        if not os.path.exists(self.path):
            os.mkdir(self.path)
        os.mkdir(self.htdocs_dir)
        os.mkdir(self.log_dir)
        os.mkdir(self.plugins_dir)
        os.mkdir(self.templates_dir)

        # Create a few files
        create_file(os.path.join(self.path, 'VERSION'), _VERSION + '\n')
        create_file(
            os.path.join(self.path, 'README'),
            'This directory contains a Trac environment.\n'
            'Visit http://trac.edgewall.org/ for more information.\n')

        # Setup the default configuration
        os.mkdir(self.conf_dir)
        config = Configuration(self.config_file_path)
        for section, name, value in options:
            config.set(section, name, value)
        config.save()
        self.setup_config()
        if not any((section, option) == ('inherit', 'file')
                   for section, option, value in options):
            self.config.set_defaults(self)
            self.config.save()

        # Create the sample configuration
        create_file(self.config_file_path + '.sample')
        self._update_sample_config()

        # Create the database
        DatabaseManager(self).init_db()

    @lazy
    def database_version(self):
        """Returns the current version of the database.

        :since 1.0.2:
        """
        return DatabaseManager(self) \
               .get_database_version('database_version')

    @lazy
    def database_initial_version(self):
        """Returns the version of the database at the time of creation.

        In practice, for database created before 0.11, this will
        return `False` which is "older" than any db version number.

        :since 1.0.2:
        """
        return DatabaseManager(self) \
               .get_database_version('initial_database_version')

    @lazy
    def trac_version(self):
        """Returns the version of Trac.
        :since: 1.2
        """
        from trac import core, __version__
        return get_pkginfo(core).get('version', __version__)

    def setup_config(self):
        """Load the configuration file."""
        self.config = Configuration(self.config_file_path,
                                    {'envname': self.name})
        if not self.config.exists:
            raise TracError(
                _("The configuration file is not found at "
                  "%(path)s",
                  path=self.config_file_path))
        self.setup_log()
        plugins_dir = self.shared_plugins_dir
        load_components(self, plugins_dir and (plugins_dir, ))

    @lazy
    def config_file_path(self):
        """Path of the trac.ini file."""
        return os.path.join(self.conf_dir, 'trac.ini')

    @lazy
    def log_file_path(self):
        """Path to the log file."""
        if not os.path.isabs(self.log_file):
            return os.path.join(self.log_dir, self.log_file)
        return self.log_file

    def _get_path_to_dir(self, *dirs):
        path = self.path
        for dir in dirs:
            path = os.path.join(path, dir)
        return os.path.realpath(path)

    @lazy
    def attachments_dir(self):
        """Absolute path to the attachments directory.

        :since: 1.3.1
        """
        return self._get_path_to_dir('files', 'attachments')

    @lazy
    def conf_dir(self):
        """Absolute path to the conf directory.

        :since: 1.0.11
        """
        return self._get_path_to_dir('conf')

    @lazy
    def files_dir(self):
        """Absolute path to the files directory.

        :since: 1.3.2
        """
        return self._get_path_to_dir('files')

    @lazy
    def htdocs_dir(self):
        """Absolute path to the htdocs directory.

        :since: 1.0.11
        """
        return self._get_path_to_dir('htdocs')

    @lazy
    def log_dir(self):
        """Absolute path to the log directory.

        :since: 1.0.11
        """
        return self._get_path_to_dir('log')

    @lazy
    def plugins_dir(self):
        """Absolute path to the plugins directory.

        :since: 1.0.11
        """
        return self._get_path_to_dir('plugins')

    @lazy
    def templates_dir(self):
        """Absolute path to the templates directory.

        :since: 1.0.11
        """
        return self._get_path_to_dir('templates')

    def setup_log(self):
        """Initialize the logging sub-system."""
        self.log, log_handler = \
            self.create_logger(self.log_type, self.log_file_path,
                               self.log_level, self.log_format)
        self.log.addHandler(log_handler)
        self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
                      self.trac_version)

    def create_logger(self, log_type, log_file, log_level, log_format):
        log_id = 'Trac.%s' % hashlib.sha1(self.path).hexdigest()
        if log_format:
            log_format = log_format.replace('$(', '%(') \
                                   .replace('%(path)s', self.path) \
                                   .replace('%(basename)s', self.name) \
                                   .replace('%(project)s', self.project_name)
        return log.logger_handler_factory(log_type,
                                          log_file,
                                          log_level,
                                          log_id,
                                          format=log_format)

    def get_known_users(self, as_dict=False):
        """Returns information about all known users, i.e. users that
        have logged in to this Trac environment and possibly set their
        name and email.

        By default this function returns a iterator that yields one
        tuple for every user, of the form (username, name, email),
        ordered alpha-numerically by username. When `as_dict` is `True`
        the function returns a dictionary mapping username to a
        (name, email) tuple.

        :since 1.2: the `as_dict` parameter is available.
        """
        return self._known_users_dict if as_dict else iter(self._known_users)

    @cached
    def _known_users(self):
        return self.db_query("""
                SELECT DISTINCT s.sid, n.value, e.value
                FROM session AS s
                 LEFT JOIN session_attribute AS n ON (n.sid=s.sid
                  AND n.authenticated=1 AND n.name = 'name')
                 LEFT JOIN session_attribute AS e ON (e.sid=s.sid
                  AND e.authenticated=1 AND e.name = 'email')
                WHERE s.authenticated=1 ORDER BY s.sid
        """)

    @cached
    def _known_users_dict(self):
        return {u[0]: (u[1], u[2]) for u in self._known_users}

    def invalidate_known_users_cache(self):
        """Clear the known_users cache."""
        del self._known_users
        del self._known_users_dict

    def backup(self, dest=None):
        """Create a backup of the database.

        :param dest: Destination file; if not specified, the backup is
                     stored in a file called db_name.trac_version.bak
        """
        return DatabaseManager(self).backup(dest)

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        for participant in self.setup_participants:
            with self.component_guard(participant, reraise=True):
                if participant.environment_needs_upgrade():
                    self.log.warning(
                        "Component %s requires environment upgrade",
                        participant)
                    return True
        return False

    def upgrade(self, backup=False, backup_dest=None):
        """Upgrade database.

        :param backup: whether or not to backup before upgrading
        :param backup_dest: name of the backup file
        :return: whether the upgrade was performed
        """
        upgraders = []
        for participant in self.setup_participants:
            with self.component_guard(participant, reraise=True):
                if participant.environment_needs_upgrade():
                    upgraders.append(participant)
        if not upgraders:
            return

        if backup:
            try:
                self.backup(backup_dest)
            except Exception as e:
                raise BackupError(e)

        for participant in upgraders:
            self.log.info("upgrading %s...", participant)
            with self.component_guard(participant, reraise=True):
                participant.upgrade_environment()
            # Database schema may have changed, so close all connections
            dbm = DatabaseManager(self)
            if dbm.connection_uri != 'sqlite::memory:':
                dbm.shutdown()

        self._update_sample_config()
        del self.database_version
        return True

    @lazy
    def href(self):
        """The application root path"""
        return Href(urlsplit(self.abs_href.base).path)

    @lazy
    def abs_href(self):
        """The application URL"""
        if not self.base_url:
            self.log.warning("base_url option not set in configuration, "
                             "generated links may be incorrect")
        return Href(self.base_url)

    def _update_sample_config(self):
        filename = os.path.join(self.config_file_path + '.sample')
        if not os.path.isfile(filename):
            return
        config = Configuration(filename)
        config.set_defaults()
        try:
            config.save()
        except IOError as e:
            self.log.warning("Couldn't write sample configuration file (%s)%s",
                             e, exception_to_unicode(e, traceback=True))
        else:
            self.log.info(
                "Wrote sample configuration file with the new "
                "settings and their default values: %s", filename)
Ejemplo n.º 14
0
class TimelineWidget(WidgetBase):
    """Display activity feed.
    """
    default_count = IntOption(
        'widget_activity', 'limit', 25,
        """Maximum number of items displayed by default""")

    event_filters = ExtensionPoint(ITimelineEventsFilter)

    _filters_map = None

    @property
    def filters_map(self):
        """Quick access to timeline events filters to be applied for a 
        given timeline provider.
        """
        if self._filters_map is None:
            self._filters_map = {}
            for _filter in self.event_filters:
                providers = _filter.supported_providers()
                if providers is None:
                    providers = [None]
                for p in providers:
                    self._filters_map.setdefault(p, []).append(_filter)
        return self._filters_map

    def get_widget_params(self, name):
        """Return a dictionary containing arguments specification for
        the widget with specified name.
        """
        return {
            'from': {
                'desc': """Display events before this date""",
                'type': DateField(),  # TODO: Custom datetime format
            },
            'daysback': {
                'desc': """Event time window""",
                'type': int,
            },
            'precision': {
                'desc': """Time precision""",
                'type': EnumField('second', 'minute', 'hour')
            },
            'doneby': {
                'desc': """Filter events related to user""",
            },
            'filters': {
                'desc': """Event filters""",
                'type': ListField()
            },
            'max': {
                'desc': """Limit the number of events displayed""",
                'type': int
            },
            'realm': {
                'desc': """Resource realm. Used to filter events""",
            },
            'id': {
                'desc': """Resource ID. Used to filter events""",
            },
        }

    get_widget_params = pretty_wrapper(get_widget_params, check_widget_name)

    def render_widget(self, name, context, options):
        """Gather timeline events and render data in compact view
        """
        data = None
        req = context.req
        try:
            timemdl = self.env[TimelineModule]
            admin_page = tag.a(_("administration page."),
                               title=_("Plugin Administration Page"),
                               href=req.href.admin('general/plugin'))
            if timemdl is None:
                return 'widget_alert.html', {
                    'title': _("Activity"),
                    'data': {
                        'msglabel':
                        "Warning",
                        'msgbody':
                        tag_(
                            "The TimelineWidget is disabled because the "
                            "Timeline component is not available. "
                            "Is the component disabled? "
                            "You can enable from the %(page)s",
                            page=admin_page),
                        'dismiss':
                        False,
                    }
                }, context

            params = ('from', 'daysback', 'doneby', 'precision', 'filters',
                      'max', 'realm', 'id')
            start, days, user, precision, filters, count, realm, rid = \
                self.bind_params(name, options, *params)
            if context.resource.realm == 'ticket':
                if days is None:
                    # calculate a long enough time daysback
                    ticket = Ticket(self.env, context.resource.id)
                    ticket_age = datetime.now(utc) - ticket.time_created
                    days = ticket_age.days + 1
                if count is None:
                    # ignore short count for ticket feeds
                    count = 0
            if count is None:
                count = self.default_count

            fakereq = dummy_request(self.env, req.authname)
            fakereq.args = {
                'author': user or '',
                'daysback': days or '',
                'max': count,
                'precision': precision,
                'user': user
            }
            if filters:
                fakereq.args.update(dict((k, True) for k in filters))
            if start is not None:
                fakereq.args['from'] = start.strftime('%x %X')

            wcontext = context.child()
            if (realm, rid) != (None, None):
                # Override rendering context
                resource = Resource(realm, rid)
                if resource_exists(self.env, resource) or \
                        realm == rid == '':
                    wcontext = context.child(resource)
                    wcontext.req = req
                else:
                    self.log.warning("TimelineWidget: Resource %s not found",
                                     resource)
            # FIXME: Filter also if existence check is not conclusive ?
            if resource_exists(self.env, wcontext.resource):
                module = FilteredTimeline(self.env, wcontext)
                self.log.debug('Filtering timeline events for %s',
                               wcontext.resource)
            else:
                module = timemdl
            data = module.process_request(fakereq)[1]
        except TracError, exc:
            if data is not None:
                exc.title = data.get('title', 'Activity')
            raise
        else:
Ejemplo n.º 15
0
Archivo: api.py Proyecto: wataash/trac
class Mimeview(Component):
    """Generic HTML renderer for data, typically source code."""

    required = True

    renderers = ExtensionPoint(IHTMLPreviewRenderer)
    annotators = ExtensionPoint(IHTMLPreviewAnnotator)
    converters = ExtensionPoint(IContentConverter)

    default_charset = Option('trac', 'default_charset', 'utf-8',
                             """Charset to be used when in doubt.""")

    tab_width = IntOption('mimeviewer', 'tab_width', 8,
                          """Displayed tab width in file preview.""")

    max_preview_size = IntOption('mimeviewer', 'max_preview_size', 262144,
                                 """Maximum file size for HTML preview.""")

    mime_map = ListOption(
        'mimeviewer',
        'mime_map',
        'text/x-dylan:dylan, text/x-idl:ice, text/x-ada:ads:adb',
        doc="""List of additional MIME types and keyword mappings.
        Mappings are comma-separated, and for each MIME type,
        there's a colon (":") separated list of associated keywords
        or file extensions.
        """)

    mime_map_patterns = ListOption(
        'mimeviewer',
        'mime_map_patterns',
        'text/plain:README(?!\.rst)|INSTALL(?!\.rst)|COPYING.*',
        doc="""List of additional MIME types associated to filename patterns.
        Mappings are comma-separated, and each mapping consists of a MIME type
        and a Python regexp used for matching filenames, separated by a colon
        (":"). (''since 1.0'')
        """)

    treat_as_binary = ListOption(
        'mimeviewer',
        'treat_as_binary',
        'application/octet-stream, application/pdf, application/postscript, '
        'application/msword, application/rtf',
        doc="""Comma-separated list of MIME types that should be treated as
        binary data.
        """)

    def __init__(self):
        self._mime_map = None
        self._mime_map_patterns = None

    # Public API

    def get_supported_conversions(self, mimetype):
        """Return a list of target MIME types as instances of the `namedtuple`
        `MimeConversion`. Output is ordered from best to worst quality.

        The `MimeConversion` `namedtuple` has fields: key, name, extension,
        in_mimetype, out_mimetype, quality, converter.
        """
        fields = ('key', 'name', 'extension', 'in_mimetype', 'out_mimetype',
                  'quality', 'converter')
        _MimeConversion = namedtuple('MimeConversion', fields)
        converters = []
        for c in self.converters:
            for k, n, e, im, om, q in c.get_supported_conversions() or []:
                if im == mimetype and q > 0:
                    converters.append(_MimeConversion(k, n, e, im, om, q, c))
        converters = sorted(converters, key=lambda i: i.quality, reverse=True)
        return converters

    def convert_content(self,
                        req,
                        mimetype,
                        content,
                        key,
                        filename=None,
                        url=None,
                        iterable=False):
        """Convert the given content to the target MIME type represented by
        `key`, which can be either a MIME type or a key. Returns a tuple of
        (content, output_mime_type, extension)."""
        if not content:
            return '', 'text/plain;charset=utf-8', '.txt'

        # Ensure we have a MIME type for this content
        full_mimetype = mimetype
        if not full_mimetype:
            if hasattr(content, 'read'):
                content = content.read(self.max_preview_size)
            full_mimetype = self.get_mimetype(filename, content)
        if full_mimetype:
            mimetype = ct_mimetype(full_mimetype)  # split off charset
        else:
            mimetype = full_mimetype = 'text/plain'  # fallback if not binary

        # Choose best converter
        candidates = [
            c for c in self.get_supported_conversions(mimetype)
            if key in (c.key, c.out_mimetype)
        ]
        if not candidates:
            raise TracError(
                _("No available MIME conversions from %(old)s to %(new)s",
                  old=mimetype,
                  new=key))

        # First successful conversion wins
        for conversion in candidates:
            output = conversion.converter.convert_content(
                req, mimetype, content, conversion.key)
            if output:
                content, content_type = output
                if iterable:
                    if isinstance(content, basestring):
                        content = (content, )
                else:
                    if not isinstance(content, basestring):
                        content = ''.join(content)
                return content, content_type, conversion.extension
        raise TracError(
            _("No available MIME conversions from %(old)s to %(new)s",
              old=mimetype,
              new=key))

    def get_annotation_types(self):
        """Generator that returns all available annotation types."""
        for annotator in self.annotators:
            yield annotator.get_annotation_type()

    def render(self,
               context,
               mimetype,
               content,
               filename=None,
               url=None,
               annotations=None,
               force_source=False):
        """Render an XHTML preview of the given `content`.

        `content` is the same as an `IHTMLPreviewRenderer.render`'s
        `content` argument.

        The specified `mimetype` will be used to select the most appropriate
        `IHTMLPreviewRenderer` implementation available for this MIME type.
        If not given, the MIME type will be infered from the filename or the
        content.

        Return a string containing the XHTML text.

        When rendering with an `IHTMLPreviewRenderer` fails, a warning is added
        to the request associated with the context (if any), unless the
        `disable_warnings` hint is set to `True`.
        """
        if not content:
            return ''

        # Ensure we have a MIME type for this content
        full_mimetype = mimetype
        if not full_mimetype:
            if hasattr(content, 'read'):
                content = content.read(self.max_preview_size)
            full_mimetype = self.get_mimetype(filename, content)
        if full_mimetype:
            mimetype = ct_mimetype(full_mimetype)  # split off charset
        else:
            mimetype = full_mimetype = 'text/plain'  # fallback if not binary

        # Determine candidate `IHTMLPreviewRenderer`s
        candidates = []
        for renderer in self.renderers:
            qr = renderer.get_quality_ratio(mimetype)
            if qr > 0:
                candidates.append((qr, renderer))
        candidates.sort(key=lambda item: -item[0])

        # Wrap file-like object so that it can be read multiple times
        if hasattr(content, 'read'):
            content = Content(content, self.max_preview_size)

        # First candidate which renders successfully wins.
        # Also, we don't want to expand tabs more than once.
        expanded_content = None
        for qr, renderer in candidates:
            if force_source and not getattr(renderer, 'returns_source', False):
                continue  # skip non-source renderers in force_source mode
            if isinstance(content, Content):
                content.reset()
            ann_names = ', '.join(annotations) if annotations else \
                       'no annotations'
            self.log.debug('Trying to render HTML preview using %s [%s]',
                           renderer.__class__.__name__, ann_names)

            # check if we need to perform a tab expansion
            rendered_content = content
            if getattr(renderer, 'expand_tabs', False):
                if expanded_content is None:
                    content = content_to_unicode(self.env, content,
                                                 full_mimetype)
                    expanded_content = content.expandtabs(self.tab_width)
                rendered_content = expanded_content
            try:
                result = renderer.render(context, full_mimetype,
                                         rendered_content, filename, url)
            except Exception as e:
                self.log.warning('HTML preview using %s with %r failed: %s',
                                 renderer.__class__.__name__, context,
                                 exception_to_unicode(e, traceback=True))
                if context.req and not context.get_hint('disable_warnings'):
                    from trac.web.chrome import add_warning
                    add_warning(
                        context.req,
                        _("HTML preview using %(renderer)s failed (%(err)s)",
                          renderer=renderer.__class__.__name__,
                          err=exception_to_unicode(e)))
            else:
                if not result:
                    continue

                if not (force_source
                        or getattr(renderer, 'returns_source', False)):
                    # Direct rendering of content
                    if isinstance(result, basestring):
                        return Markup(to_unicode(result))
                    elif isinstance(result, Fragment):
                        return result.__html__()
                    else:
                        return result

                # Render content as source code
                if annotations:
                    marks = context.req.args.get('marks') if context.req \
                            else None
                    if marks:
                        context.set_hints(marks=marks)
                    return self._render_source(context, result, annotations)
                else:
                    if isinstance(result, list):
                        result = Markup('\n').join(result)
                    return tag.div(class_='code')(tag.pre(result))

    def _render_source(self, context, lines, annotations):
        from trac.web.chrome import add_warning
        annotators, labels, titles = {}, {}, {}
        for annotator in self.annotators:
            atype, alabel, atitle = annotator.get_annotation_type()
            if atype in annotations:
                labels[atype] = alabel
                titles[atype] = atitle
                annotators[atype] = annotator
        annotations = [a for a in annotations if a in annotators]

        if isinstance(lines, unicode):
            lines = lines.splitlines(True)
        # elif isinstance(lines, list):
        #    pass # assume these are lines already

        annotator_datas = []
        for a in annotations:
            annotator = annotators[a]
            try:
                data = (annotator, annotator.get_annotation_data(context))
            except TracError as e:
                self.log.warning("Can't use annotator '%s': %s", a, e)
                add_warning(
                    context.req,
                    tag.strong(
                        tag_("Can't use %(annotator)s annotator: %(error)s",
                             annotator=tag.em(a),
                             error=tag.pre(e))))
                data = None, None
            annotator_datas.append(data)

        def _head_row():
            return tag.tr([
                tag.th(labels[a], class_=a, title=titles[a])
                for a in annotations
            ] + [tag.th(u'\xa0', class_='content')])

        def _body_rows():
            for idx, line in enumerate(lines):
                row = tag.tr()
                for annotator, data in annotator_datas:
                    if annotator:
                        annotator.annotate_row(context, row, idx + 1, line,
                                               data)
                    else:
                        row.append(tag.td())
                row.append(tag.td(line))
                yield row

        return tag.table(class_='code')(tag.thead(_head_row()),
                                        tag.tbody(_body_rows()))

    def get_charset(self, content='', mimetype=None):
        """Infer the character encoding from the `content` or the `mimetype`.

        `content` is either a `str` or an `unicode` object.

        The charset will be determined using this order:
         * from the charset information present in the `mimetype` argument
         * auto-detection of the charset from the `content`
         * the configured `default_charset`
        """
        if mimetype:
            ctpos = mimetype.find('charset=')
            if ctpos >= 0:
                return mimetype[ctpos + 8:].strip()
        if isinstance(content, str):
            utf = detect_unicode(content)
            if utf is not None:
                return utf
        return self.default_charset

    @property
    def mime_map(self):
        # Extend default extension to MIME type mappings with configured ones
        if not self._mime_map:
            self._mime_map = MIME_MAP.copy()
            # augment mime_map from `IHTMLPreviewRenderer`s
            for renderer in self.renderers:
                if hasattr(renderer, 'get_extra_mimetypes'):
                    for mimetype, kwds in renderer.get_extra_mimetypes() or []:
                        self._mime_map[mimetype] = mimetype
                        for keyword in kwds:
                            self._mime_map[keyword] = mimetype
            # augment/override mime_map from trac.ini
            for mapping in self.config['mimeviewer'].getlist('mime_map'):
                if ':' in mapping:
                    assocations = mapping.split(':')
                    for keyword in assocations:  # Note: [0] kept on purpose
                        self._mime_map[keyword] = assocations[0]
        return self._mime_map

    def get_mimetype(self, filename, content=None):
        """Infer the MIME type from the `filename` or the `content`.

        `content` is either a `str` or an `unicode` object.

        Return the detected MIME type, augmented by the
        charset information (i.e. "<mimetype>; charset=..."),
        or `None` if detection failed.
        """

        mimetype = get_mimetype(filename, content, self.mime_map,
                                self.mime_map_patterns)
        charset = None
        if mimetype:
            charset = self.get_charset(content, mimetype)
        if mimetype and charset and 'charset' not in mimetype:
            mimetype += '; charset=' + charset
        return mimetype

    @property
    def mime_map_patterns(self):
        if not self._mime_map_patterns:
            self._mime_map_patterns = {}
            for mapping in self.config['mimeviewer'] \
                    .getlist('mime_map_patterns'):
                if ':' in mapping:
                    mimetype, regexp = mapping.split(':', 1)
                try:
                    self._mime_map_patterns[mimetype] = re.compile(regexp)
                except re.error as e:
                    self.log.warning(
                        "mime_map_patterns contains invalid "
                        "regexp '%s' for mimetype '%s' (%s)", regexp, mimetype,
                        exception_to_unicode(e))
        return self._mime_map_patterns

    def is_binary(self, mimetype=None, filename=None, content=None):
        """Check if a file must be considered as binary."""
        if not mimetype and filename:
            mimetype = self.get_mimetype(filename, content)
        if mimetype:
            mimetype = ct_mimetype(mimetype)
            if mimetype in self.treat_as_binary:
                return True
        if content is not None and is_binary(content):
            return True
        return False

    def to_unicode(self, content, mimetype=None, charset=None):
        """Convert `content` (an encoded `str` object) to an `unicode` object.

        This calls `trac.util.to_unicode` with the `charset` provided,
        or the one obtained by `Mimeview.get_charset()`.
        """
        if not charset:
            charset = self.get_charset(content, mimetype)
        return to_unicode(content, charset)

    def configured_modes_mapping(self, renderer):
        """Return a MIME type to `(mode,quality)` mapping for given `option`"""
        types, option = {}, '%s_modes' % renderer
        for mapping in self.config['mimeviewer'].getlist(option):
            if not mapping:
                continue
            try:
                mimetype, mode, quality = mapping.split(':')
                types[mimetype] = (mode, int(quality))
            except (TypeError, ValueError):
                self.log.warning(
                    "Invalid mapping '%s' specified in '%s' "
                    "option.", mapping, option)
        return types

    def preview_data(self,
                     context,
                     content,
                     length,
                     mimetype,
                     filename,
                     url=None,
                     annotations=None,
                     force_source=False):
        """Prepares a rendered preview of the given `content`.

        Note: `content` will usually be an object with a `read` method.
        """
        data = {
            'raw_href': url,
            'size': length,
            'max_file_size': self.max_preview_size,
            'max_file_size_reached': False,
            'rendered': None,
        }
        if length >= self.max_preview_size:
            data['max_file_size_reached'] = True
        else:
            result = self.render(context,
                                 mimetype,
                                 content,
                                 filename,
                                 url,
                                 annotations,
                                 force_source=force_source)
            data['rendered'] = result
        return data

    def send_converted(self, req, in_type, content, selector, filename='file'):
        """Helper method for converting `content` and sending it directly.

        `selector` can be either a key or a MIME Type."""
        from trac.web.chrome import Chrome
        from trac.web.api import RequestDone
        iterable = Chrome(self.env).use_chunked_encoding
        content, output_type, ext = self.convert_content(req,
                                                         in_type,
                                                         content,
                                                         selector,
                                                         iterable=iterable)
        if iterable:

            def encoder(content):
                for chunk in content:
                    if isinstance(chunk, unicode):
                        chunk = chunk.encode('utf-8')
                    yield chunk

            content = encoder(content)
            length = None
        else:
            if isinstance(content, unicode):
                content = content.encode('utf-8')
            length = len(content)
        req.send_response(200)
        req.send_header('Content-Type', output_type)
        if length is not None:
            req.send_header('Content-Length', length)
        if filename:
            req.send_header(
                'Content-Disposition',
                content_disposition('attachment', '%s.%s' % (filename, ext)))
        req.end_headers()
        req.write(content)
        raise RequestDone
Ejemplo n.º 16
0
class AgiloInit(PluginEnvironmentSetup):
    """ Initialize database and environment for link component """

    setup_listeners = ExtensionPoint(IAgiloEnvironmentSetupListener)

    # PluginEnvironmentSetup template methods

    def get_db_version(self, db):
        """Returns the normalized db version (integer). This method can convert 
        the decimal numbers from Agilo 0.6 to integer."""
        fetch_version = super(AgiloInit, self)._fetch_db_version
        db_version = fetch_version(db, self.name)
        if db_version == 0:
            # Agilo versions before 0.7 had different database versions with
            # floating point
            old_version = fetch_version(db, name='agilo-types')
            if old_version == '1.2':
                db_version = 1
            elif old_version != 0:
                msg = _('Unknown version for agilo-types: %s') % old_version
                raise TracError(msg)
        elif db_version == '0.7':
            db_version = 2
        # 'Modern' Agilo versions like 0.7 just return something like '3'.
        db_version = int(db_version)
        return db_version

    def get_package_name(self):
        return 'agilo.db.upgrades'

    def get_expected_db_version(self):
        return db_default.db_version

    def name(self):
        # Do not modify this because it is used in the DB!
        return 'agilo'

    name = property(name)

    def set_db_version(self, db):
        cursor = db.cursor()
        latest_version = self.get_expected_db_version()
        # If there is an update from 0.6 -> 0.7 we have a version number 1 but
        # 'agilo' does not exist in the DB.
        was_upgrade = (self.get_db_version(db) > 1)
        if was_upgrade:
            self._update_version_number(cursor, latest_version)
        else:
            self._insert_version_number(cursor, latest_version)

    #==========================================================================
    # IEnvironmentSetupParticipant
    #==========================================================================
    def environment_created(self):
        for table in db_default.schema:
            create_table(self.env, table)

        cache_manager = HttpRequestCacheManager(self.env)
        po_manager = PersistentObjectManager(self.env)
        for manager in cache_manager.managers:
            model_class = manager.for_model()
            if issubclass(model_class, PersistentObject):
                module_name = model_class.__module__
                # We don't want to create tables for dummy classes automatically
                # but the test finder may load some of these managers so we
                # need to exclude them here.
                if ('tests.' not in module_name):
                    po_manager.create_table(model_class)

        # Need to create Agilo types in the database before writing to the
        # configuration - otherwise we get a warning during config setup (you'll
        # see it in every test case during setUp)
        db_default.create_default_types(self.env)
        initialize_config(self.env, __CONFIG_PROPERTIES__)
        db_default.create_default_backlogs(self.env)
        super(AgiloInit, self).environment_created()
        for listener in self.setup_listeners:
            listener.agilo_was_installed()
        # Reload the AgiloConfig to make sure all the changes have been updated
        AgiloConfig(self.env).reload()
        info(self, 'Agilo environment initialized')
Ejemplo n.º 17
0
class BloodhoundSearchApi(Component):
    """Implements core indexing functionality, provides methods for
    searching, adding and deleting documents from index.
    """
    implements(IEnvironmentSetupParticipant, ISupportMultiProductEnvironment)

    backend = ExtensionOption(
        'bhsearch', 'search_backend', ISearchBackend, 'WhooshBackend',
        'Name of the component implementing Bloodhound Search backend \
        interface: ISearchBackend.')

    parser = ExtensionOption(
        'bhsearch', 'query_parser', IQueryParser, 'DefaultQueryParser',
        'Name of the component implementing Bloodhound Search query \
        parser.')

    index_pre_processors = OrderedExtensionsOption(
        'bhsearch',
        'index_preprocessors',
        IDocIndexPreprocessor,
        ['SecurityPreprocessor'],
    )
    result_post_processors = ExtensionPoint(IResultPostprocessor)
    query_processors = ExtensionPoint(IQueryPreprocessor)

    index_participants = MultiProductExtensionPoint(IIndexParticipant)

    def query(self,
              query,
              sort=None,
              fields=None,
              filter=None,
              facets=None,
              pagenum=1,
              pagelen=20,
              highlight=False,
              highlight_fields=None,
              context=None):
        """Return query result from an underlying search backend.

        Arguments:
        :param query: query string e.g. “bla status:closed” or a parsed
            representation of the query.
        :param sort: optional sorting
        :param boost: optional list of fields with boost values e.g.
            {“id”: 1000, “subject” :100, “description”:10}.
        :param filter: optional list of terms. Usually can be cached by
            underlying search framework. For example {“type”: “wiki”}
        :param facets: optional list of facet terms, can be field or expression
        :param page: paging support
        :param pagelen: paging support
        :param highlight: highlight matched terms in fields
        :param highlight_fields: list of fields to highlight
        :param context: request context

        :return: result QueryResult
        """
        # pylint: disable=too-many-locals
        self.env.log.debug("Receive query request: %s", locals())

        parsed_query = self.parser.parse(query, context)

        parsed_filters = self.parser.parse_filters(filter)
        # TODO: add query parsers and meta keywords post-parsing

        # TODO: apply security filters

        query_parameters = dict(
            query=parsed_query,
            query_string=query,
            sort=sort,
            fields=fields,
            filter=parsed_filters,
            facets=facets,
            pagenum=pagenum,
            pagelen=pagelen,
            highlight=highlight,
            highlight_fields=highlight_fields,
        )
        for query_processor in self.query_processors:
            query_processor.query_pre_process(query_parameters, context)

        query_result = self.backend.query(**query_parameters)

        for post_processor in self.result_post_processors:
            post_processor.post_process(query_result)

        query_result.debug["api_parameters"] = query_parameters
        return query_result

    def start_operation(self):
        return self.backend.start_operation()

    def rebuild_index(self):
        """Rebuild underlying index"""
        self.log.info('Rebuilding the search index.')
        self.backend.recreate_index()
        with self.backend.start_operation() as operation_context:
            doc = None
            try:
                for participant in self.index_participants:
                    self.log.info(
                        "Reindexing resources provided by %s in product %s" %
                        (participant.__class__.__name__,
                         getattr(participant.env.product, 'name', "''")))
                    docs = participant.get_entries_for_index()
                    for doc in docs:
                        self.log.debug("Indexing document %s:%s/%s" % (
                            doc['product'],
                            doc['type'],
                            doc['id'],
                        ))
                        self.add_doc(doc, operation_context)
                self.log.info("Reindexing complete.")
            except Exception, ex:
                self.log.error(ex)
                if doc:
                    self.log.error("Doc that triggers the error: %s" % doc)
                raise
Ejemplo n.º 18
0
class UserProfileBox(Component):
    implements(IRequestHandler, ITemplateProvider, IUserProfileActions,
               IRequestFilter)
    profile_action_providers = ExtensionPoint(IUserProfileActions)

    # IRequestHandler methods

    def match_request(self, req):
        return req.path_info.endswith('/profilebox')

    def process_request(self, req):
        """
        Handles the profile box request, which is expected to be in format::

            /user/<username>/profilebox

        :returns:
            Pre-rendered user profile box HTML using templates:

            - multiproject_user_profilebox.html: To show the content
            - multiproject_user_profilebox_default.html: In a case of failures, missing data etc
        """
        # Read and validate arguments
        #account_id = req.args.get('user_id', None)
        #account_username = req.args.get('username', None)

        match = MATCHREGX.match(req.path_info)
        if not match:
            msg = _('Account cannot be found')
            return 'multiproject_user_profilebox_default.html', {
                'msg': msg
            }, False

        # Load specified account
        userstore = get_userstore()
        account = userstore.getUser(match.group('username'))
        if not account:
            msg = _('Account cannot be found')
            return 'multiproject_user_profilebox_default.html', {
                'msg': msg
            }, False

        # Check if user has USER_VIEW permission to view other users in home project
        homeperm = self._get_home_perm(req)
        if req.authname != account.username and 'USER_VIEW' not in homeperm:
            msg = _('Access denied')
            return 'multiproject_user_profilebox_default.html', {
                'msg': msg
            }, False

        # Load registered actions
        actions = []
        for aprovider in self.profile_action_providers:
            # Iterate actions and validate them
            for new_action in aprovider.get_profile_actions(req, account):
                # Ensure each action is in tuple format: (order, fragment)
                if not isinstance(new_action, tuple):
                    new_action = (0, new_action)
                # Add to list
                actions.append(new_action)

        # Sort the actions by hints given in actions:
        # the smaller the value, the higher the priority
        actions.sort(key=lambda tup: tup[0])

        # Construct list items: put first and class values
        litems = []
        llen = len(actions)
        for index, action in enumerate(actions):
            classes = []
            # If last
            if index == 0:
                classes.append('first')
            if llen == index + 1:
                classes.append('last')

            litems.append(tag.li(action[1], **{'class': ' '.join(classes)}))

        # If empty, put empty list element in place for easier styling
        if not litems:
            litems.append(tag.li('li', **{'class': 'first last'}))

        # Pass data to template. Generate ul/li list from registered actions
        data = {'account': account, 'actionlist': tag.ul(*litems)}

        return 'multiproject_user_profilebox.html', data, False

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return [('multiproject',
                 pkg_resources.resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return [
            pkg_resources.resource_filename('multiproject.common.users',
                                            'templates')
        ]

    # IUserProfileActions

    def get_profile_actions(self, req, user):
        """
        Return list of actions
        """
        actions = []
        homeperm = self._get_home_perm(req)
        uresource = Resource('user', user.id)

        # Project settings for own account
        if user.username == req.authname:
            actions.append((-40,
                            tag.a(_('View your projects'),
                                  href=homeperm.env.href('myprojects'))))
            actions.append((0,
                            tag.a(_('Edit your settings'),
                                  href=homeperm.env.href('prefs'))))
        # View other user profile
        else:
            actions.append(
                (-50,
                 tag.a(_('View service profile'),
                       href=homeperm.env.href('user', user.username))))

        # If user can fully manage account
        if homeperm.has_permission('USER_EDIT', uresource):
            label = 'Manage your account' if user.username == req.authname else 'Manage account'
            actions.append(
                (-2,
                 tag.a(_(label),
                       href=homeperm.env.href('admin/users/manage',
                                              username=user.username))))

        # Tickets assigned to or created by user (if component is enabled and user has permissions to view them)
        # Note: href.kwargs cannot be used because of reserved word 'or' and un-ordered nature of dict
        if (self.env.is_component_enabled('trac.ticket.query.QueryModule') or
            self.env.is_component_enabled('multiproject.project.tickets.viewtickets.QueryModuleInterceptor')) \
            and 'TICKET_VIEW' in req.perm:
            qstring = 'owner={0}&or&reporter={0}&group=status'.format(
                user.username)
            query = Query.from_string(self.env, qstring)
            actions.append((5, (tag.a(_('View tickets'),
                                      href=query.get_href(req.href)))))

        return actions

    def _get_home_perm(self, req):
        """
        Returns permission cache from home environment
        """
        home_env = open_environment(os.path.join(
            self.env.config.get('multiproject', 'sys_projects_root'),
            self.env.config.get('multiproject', 'sys_home_project_name')),
                                    use_cache=True)
        return PermissionCache(home_env, req.authname)

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        """
        Process request to add some data in request
        """
        return handler

    def post_process_request(self, req, template, data, content_type):
        """
        Add global javascript data on post-processing phase
        """
        # When processing template, add global javascript json into scripts
        if template:
            add_stylesheet(req, 'multiproject/css/multiproject.css')
            add_script(req, 'multiproject/js/multiproject.js')
            add_script(req, 'multiproject/js/profile.js')
            add_script(req, 'multiproject/js/preference.js')

        return template, data, content_type
Ejemplo n.º 19
0
class Environment(Component, ComponentManager):
    """Trac stores project information in a Trac environment.

    A Trac environment consists of a directory structure containing among other
    things:
     * a configuration file.
     * an SQLite database (stores tickets, wiki pages...)
     * Project specific templates and wiki macros.
     * wiki and ticket attachments.
    """
    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)

    base_url = Option(
        'trac', 'base_url', '', """Base URL of the Trac deployment.
        
        In most configurations, Trac will automatically reconstruct the URL
        that is used to access it automatically. However, in more complex
        setups, usually involving running Trac behind a HTTP proxy, you may
        need to use this option to force Trac to use the correct URL.""")

    project_name = Option('project', 'name', 'My Project',
                          """Name of the project.""")

    project_description = Option('project', 'descr', 'My example project',
                                 """Short description of the project.""")

    project_url = Option('project', 'url', 'http://example.org/',
                         """URL of the main project web site.""")

    project_footer = Option(
        'project', 'footer', 'Visit the Trac open source project at<br />'
        '<a href="http://trac.edgewall.org/">'
        'http://trac.edgewall.org/</a>',
        """Page footer text (right-aligned).""")

    project_icon = Option('project', 'icon', 'common/trac.ico',
                          """URL of the icon of the project.""")

    log_type = Option(
        'logging', 'log_type', 'none', """Logging facility to use.
        
        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")

    log_file = Option(
        'logging', 'log_file', 'trac.log',
        """If `log_type` is `file`, this should be a path to the log-file.""")

    log_level = Option(
        'logging', 'log_level', 'DEBUG', """Level of verbosity in log.
        
        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")

    log_format = Option(
        'logging', 'log_format', None, """Custom logging format.

        If nothing is set, the following will be used:
        
        Trac[$(module)s] $(levelname)s: $(message)s

        In addition to regular key names supported by the Python logger library
        library (see http://docs.python.org/lib/node422.html), one could use:
         - $(path)s     the path for the current environment
         - $(basename)s the last path component of the current environment
         - $(project)s  the project name

         Note the usage of `$(...)s` instead of `%(...)s` as the latter form
         would be interpreted by the ConfigParser itself.

         Example:
         ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s

         (since 0.11)""")

    def __init__(self, path, create=False, options=[]):
        """Initialize the Trac environment.
        
        @param path:   the absolute path to the Trac environment
        @param create: if `True`, the environment is created and populated with
                       default data; otherwise, the environment is expected to
                       already exist.
        @param options: A list of `(section, name, value)` tuples that define
                        configuration options
        """
        ComponentManager.__init__(self)

        self.path = path
        self.setup_config(load_defaults=create)
        self.setup_log()

        from trac.loader import load_components
        load_components(self)

        if create:
            self.create(options)
        else:
            self.verify()

        if create:
            for setup_participant in self.setup_participants:
                setup_participant.environment_created()

    def component_activated(self, component):
        """Initialize additional member variables for components.
        
        Every component activated through the `Environment` object gets three
        member variables: `env` (the environment object), `config` (the
        environment configuration) and `log` (a logger object)."""
        component.env = self
        component.config = self.config
        component.log = self.log

    def is_component_enabled(self, cls):
        """Implemented to only allow activation of components that are not
        disabled in the configuration.
        
        This is called by the `ComponentManager` base class when a component is
        about to be activated. If this method returns false, the component does
        not get activated."""
        if not isinstance(cls, basestring):
            component_name = (cls.__module__ + '.' + cls.__name__).lower()
        else:
            component_name = cls.lower()

        rules = [(name.lower(), value.lower() in ('enabled', 'on'))
                 for name, value in self.config.options('components')]
        rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))

        for pattern, enabled in rules:
            if component_name == pattern or pattern.endswith('*') \
                    and component_name.startswith(pattern[:-1]):
                return enabled

        # versioncontrol components are enabled if the repository is configured
        # FIXME: this shouldn't be hardcoded like this
        if component_name.startswith('trac.versioncontrol.'):
            return self.config.get('trac', 'repository_dir') != ''

        # By default, all components in the trac package are enabled
        return component_name.startswith('trac.')

    def verify(self):
        """Verify that the provided path points to a valid Trac environment
        directory."""
        fd = open(os.path.join(self.path, 'VERSION'), 'r')
        try:
            assert fd.read(26) == 'Trac Environment Version 1'
        finally:
            fd.close()

    def get_db_cnx(self):
        """Return a database connection from the connection pool."""
        return DatabaseManager(self).get_connection()

    def shutdown(self, tid=None):
        """Close the environment."""
        RepositoryManager(self).shutdown(tid)
        DatabaseManager(self).shutdown(tid)

    def get_repository(self, authname=None):
        """Return the version control repository configured for this
        environment.
        
        @param authname: user name for authorization
        """
        return RepositoryManager(self).get_repository(authname)

    def create(self, options=[]):
        """Create the basic directory structure of the environment, initialize
        the database and populate the configuration file with default values."""
        def _create_file(fname, data=None):
            fd = open(fname, 'w')
            if data: fd.write(data)
            fd.close()

        # Create the directory structure
        if not os.path.exists(self.path):
            os.mkdir(self.path)
        os.mkdir(self.get_log_dir())
        os.mkdir(self.get_htdocs_dir())
        os.mkdir(os.path.join(self.path, 'plugins'))
        os.mkdir(os.path.join(self.path, 'wiki-macros'))

        # Create a few files
        _create_file(os.path.join(self.path, 'VERSION'),
                     'Trac Environment Version 1\n')
        _create_file(
            os.path.join(self.path, 'README'),
            'This directory contains a Trac environment.\n'
            'Visit http://trac.edgewall.org/ for more information.\n')

        # Setup the default configuration
        os.mkdir(os.path.join(self.path, 'conf'))
        _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
        self.setup_config(load_defaults=True)
        for section, name, value in options:
            self.config.set(section, name, value)
        self.config.save()

        # Create the database
        DatabaseManager(self).init_db()

    def get_version(self, db=None):
        """Return the current version of the database."""
        if not db:
            db = self.get_db_cnx()
        cursor = db.cursor()
        cursor.execute(
            "SELECT value FROM system WHERE name='database_version'")
        row = cursor.fetchone()
        return row and int(row[0])

    def setup_config(self, load_defaults=False):
        """Load the configuration file."""
        self.config = Configuration(os.path.join(self.path, 'conf',
                                                 'trac.ini'))
        if load_defaults:
            for section, default_options in self.config.defaults().iteritems():
                for name, value in default_options.iteritems():
                    if self.config.has_site_option(section, name):
                        value = None
                    self.config.set(section, name, value)

    def get_templates_dir(self):
        """Return absolute path to the templates directory."""
        return os.path.join(self.path, 'templates')

    def get_htdocs_dir(self):
        """Return absolute path to the htdocs directory."""
        return os.path.join(self.path, 'htdocs')

    def get_log_dir(self):
        """Return absolute path to the log directory."""
        return os.path.join(self.path, 'log')

    def setup_log(self):
        """Initialize the logging sub-system."""
        from trac.log import logger_factory
        logtype = self.log_type
        logfile = self.log_file
        if logtype == 'file' and not os.path.isabs(logfile):
            logfile = os.path.join(self.get_log_dir(), logfile)
        format = self.log_format
        if format:
            format = format.replace('$(', '%(') \
                     .replace('%(path)s', self.path) \
                     .replace('%(basename)s', os.path.basename(self.path)) \
                     .replace('%(project)s', self.project_name)
        self.log = logger_factory(logtype,
                                  logfile,
                                  self.log_level,
                                  self.path,
                                  format=format)

    def get_known_users(self, cnx=None):
        """Generator that yields information about all known users, i.e. users
        that have logged in to this Trac environment and possibly set their name
        and email.

        This function generates one tuple for every user, of the form
        (username, name, email) ordered alpha-numerically by username.

        @param cnx: the database connection; if ommitted, a new connection is
                    retrieved
        """
        if not cnx:
            cnx = self.get_db_cnx()
        cursor = cnx.cursor()
        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
                       "FROM session AS s "
                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
                       "  and n.authenticated=1 AND n.name = 'name') "
                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
                       "  AND e.authenticated=1 AND e.name = 'email') "
                       "WHERE s.authenticated=1 ORDER BY s.sid")
        for username, name, email in cursor:
            yield username, name, email

    def backup(self, dest=None):
        """Simple SQLite-specific backup of the database.

        @param dest: Destination file; if not specified, the backup is stored in
                     a file called db_name.trac_version.bak
        """
        import shutil

        db_str = self.config.get('trac', 'database')
        if not db_str.startswith('sqlite:'):
            raise EnvironmentError('Can only backup sqlite databases')
        db_name = os.path.join(self.path, db_str[7:])
        if not dest:
            dest = '%s.%i.bak' % (db_name, self.get_version())
        shutil.copy(db_name, dest)

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        db = self.get_db_cnx()
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                self.log.warning('Component %s requires environment upgrade',
                                 participant)
                return True
        return False

    def upgrade(self, backup=False, backup_dest=None):
        """Upgrade database.
        
        Each db version should have its own upgrade module, names
        upgrades/dbN.py, where 'N' is the version number (int).

        @param backup: whether or not to backup before upgrading
        @param backup_dest: name of the backup file
        @return: whether the upgrade was performed
        """
        db = self.get_db_cnx()

        upgraders = []
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                upgraders.append(participant)
        if not upgraders:
            return False

        if backup:
            self.backup(backup_dest)
        for participant in upgraders:
            participant.upgrade_environment(db)
        db.commit()

        # Database schema may have changed, so close all connections
        self.shutdown()

        return True
Ejemplo n.º 20
0
class NotificationSystem(Component):

    email_sender = ExtensionOption(
        'notification', 'email_sender', IEmailSender, 'SmtpEmailSender',
        """Name of the component implementing `IEmailSender`.

        This component is used by the notification system to send emails.
        Trac currently provides `SmtpEmailSender` for connecting to an SMTP
        server, and `SendmailEmailSender` for running a `sendmail`-compatible
        executable.
        """)

    smtp_enabled = BoolOption('notification', 'smtp_enabled', 'false',
                              """Enable email notification.""")

    smtp_from = Option(
        'notification', 'smtp_from', 'trac@localhost',
        """Sender address to use in notification emails.

        At least one of `smtp_from` and `smtp_replyto` must be set, otherwise
        Trac refuses to send notification mails.""")

    smtp_from_name = Option('notification', 'smtp_from_name', '',
                            """Sender name to use in notification emails.""")

    smtp_from_author = BoolOption(
        'notification', 'smtp_from_author', 'false',
        """Use the author of the change as the sender in notification emails
           (e.g. reporter of a new ticket, author of a comment). If the
           author hasn't set an email address, `smtp_from` and
           `smtp_from_name` are used instead.
           (''since 1.0'')""")

    smtp_replyto = Option(
        'notification', 'smtp_replyto', 'trac@localhost',
        """Reply-To address to use in notification emails.

        At least one of `smtp_from` and `smtp_replyto` must be set, otherwise
        Trac refuses to send notification mails.""")

    smtp_always_cc_list = ListOption(
        'notification',
        'smtp_always_cc',
        '',
        sep=(',', ' '),
        doc="""Comma-separated list of email addresses to always send
               notifications to. Addresses can be seen by all recipients
               (Cc:).""")

    smtp_always_bcc_list = ListOption(
        'notification',
        'smtp_always_bcc',
        '',
        sep=(',', ' '),
        doc="""Comma-separated list of email addresses to always send
            notifications to. Addresses are not public (Bcc:).
            """)

    smtp_default_domain = Option(
        'notification', 'smtp_default_domain', '',
        """Default host/domain to append to addresses that do not specify
           one. Fully qualified addresses are not modified. The default
           domain is appended to all username/login for which an email
           address cannot be found in the user settings.""")

    ignore_domains_list = ListOption(
        'notification',
        'ignore_domains',
        '',
        doc="""Comma-separated list of domains that should not be considered
           part of email addresses (for usernames with Kerberos domains).""")

    admit_domains_list = ListOption(
        'notification',
        'admit_domains',
        '',
        doc="""Comma-separated list of domains that should be considered as
        valid for email addresses (such as localdomain).""")

    mime_encoding = Option(
        'notification', 'mime_encoding', 'none',
        """Specifies the MIME encoding scheme for emails.

        Supported values are: `none`, the default value which uses 7-bit
        encoding if the text is plain ASCII or 8-bit otherwise. `base64`,
        which works with any kind of content but may cause some issues with
        touchy anti-spam/anti-virus engine. `qp` or `quoted-printable`,
        which works best for european languages (more compact than base64) if
        8-bit encoding cannot be used.
        """)

    use_public_cc = BoolOption(
        'notification', 'use_public_cc', 'false',
        """Addresses in the To and Cc fields are visible to all recipients.

        If this option is disabled, recipients are put in the Bcc list.
        """)

    use_short_addr = BoolOption(
        'notification', 'use_short_addr', 'false',
        """Permit email address without a host/domain (i.e. username only).

        The SMTP server should accept those addresses, and either append
        a FQDN or use local delivery. See also `smtp_default_domain`. Do not
        use this option with a public SMTP server.
        """)

    smtp_subject_prefix = Option(
        'notification', 'smtp_subject_prefix', '__default__',
        """Text to prepend to subject line of notification emails.

        If the setting is not defined, then `[$project_name]` is used as the
        prefix. If no prefix is desired, then specifying an empty option
        will disable it.
        """)

    message_id_hash = Option(
        'notification', 'message_id_hash', 'md5',
        """Hash algorithm to create unique Message-ID header.
        ''(since 1.0.13)''""")

    notification_subscriber_section = ConfigSection(
        'notification-subscriber',
        """The notifications subscriptions are controlled by plugins. All
        `INotificationSubscriber` components are in charge. These components
        may allow to be configured via this section in the `trac.ini` file.

        See TracNotification for more details.

        Available subscribers:
        [[SubscriberList]]
        """)

    distributors = ExtensionPoint(INotificationDistributor)
    subscribers = ExtensionPoint(INotificationSubscriber)

    @lazy
    def subscriber_defaults(self):
        rawsubscriptions = self.notification_subscriber_section.options()
        return parse_subscriber_config(rawsubscriptions)

    def default_subscriptions(self, klass):
        for d in self.subscriber_defaults[klass]:
            yield (klass, d['distributor'], d['format'], d['priority'],
                   d['adverb'])

    def get_default_format(self, transport):
        return self.config.get('notification',
                               'default_format.' + transport) or 'text/plain'

    def get_preferred_format(self, sid, authenticated, transport):
        from trac.notification.prefs import get_preferred_format
        return get_preferred_format(self.env, sid, authenticated,
                                    transport) or \
               self.get_default_format(transport)

    def send_email(self, from_addr, recipients, message):
        """Send message to recipients via e-mail."""
        self.email_sender.send(from_addr, recipients, message)

    def notify(self, event):
        """Distribute an event to all subscriptions.

        :param event: a `NotificationEvent`
        """
        self.distribute_event(event, self.subscriptions(event))

    def distribute_event(self, event, subscriptions):
        """Distribute a event to all subscriptions.

        :param event: a `NotificationEvent`
        :param subscriptions: a list of tuples (sid, authenticated, address,
                              transport, format) where either sid or
                              address can be `None`
        """
        packages = {}
        for sid, authenticated, address, transport, format in subscriptions:
            package = packages.setdefault(transport, {})
            key = (sid, authenticated, address)
            if key in package:
                continue
            package[key] = format or self.get_preferred_format(
                sid, authenticated, transport)
        for distributor in self.distributors:
            for transport in distributor.transports():
                if transport in packages:
                    recipients = [
                        (k[0], k[1], k[2], format)
                        for k, format in packages[transport].iteritems()
                    ]
                    distributor.distribute(transport, recipients, event)

    def subscriptions(self, event):
        """Return all subscriptions for a given event.

        :return: a list of (sid, authenticated, address, transport, format)
        """
        subscriptions = []
        for subscriber in self.subscribers:
            if event.category == 'batchmodify':
                for ticket_event in event.get_ticket_change_events(self.env):
                    subscriptions.extend(
                        x for x in subscriber.matches(ticket_event) if x)
            else:
                subscriptions.extend(x for x in subscriber.matches(event) if x)

        # For each (transport, sid, authenticated) combination check the
        # subscription with the highest priority:
        # If it is "always" keep it. If it is "never" drop it.

        # sort by (transport, sid, authenticated, priority)
        ordered = sorted(subscriptions, key=itemgetter(1, 2, 3, 6))
        previous_combination = None
        for rule, transport, sid, auth, addr, fmt, prio, adverb in ordered:
            if (transport, sid, auth) == previous_combination:
                continue
            if adverb == 'always':
                self.log.debug(
                    "Adding (%s [%s]) for 'always' on rule (%s) "
                    "for (%s)", sid, auth, rule, transport)
                yield (sid, auth, addr, transport, fmt)
            else:
                self.log.debug(
                    "Ignoring (%s [%s]) for 'never' on rule (%s) "
                    "for (%s)", sid, auth, rule, transport)
            # Also keep subscriptions without sid (raw email subscription)
            if sid:
                previous_combination = (transport, sid, auth)
Ejemplo n.º 21
0
class SubscriptionManagementPanel(AnnouncerTemplateProvider):

    implements(IPreferencePanelProvider, ITemplateStreamFilter)

    subscribers = ExtensionPoint(IAnnouncementSubscriber)
    default_subscribers = ExtensionPoint(IAnnouncementDefaultSubscriber)
    distributors = ExtensionPoint(IAnnouncementDistributor)
    formatters = ExtensionPoint(IAnnouncementFormatter)

    def __init__(self):
        self.post_handlers = {
            'add-rule': self._add_rule,
            'delete-rule': self._delete_rule,
            'move-rule': self._move_rule,
            'set-format': self._set_format
        }

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        yield 'subscriptions', _("Subscriptions")

    def render_preference_panel(self, req, panel, path_info=None):
        if req.method == 'POST':
            method_arg = req.args.get('method', '')
            m = re.match('^([^_]+)_(.+)', method_arg)
            if m:
                method, arg = m.groups()
                method_func = self.post_handlers.get(method)
                if method_func:
                    method_func(arg, req)
                else:
                    pass
            else:
                pass
            # Refresh page after saving changes.
            req.redirect(req.href.prefs('subscriptions'))

        data = {
            'rules': {},
            'subscribers': [],
            'formatters': ('text/plain', 'text/html'),
            'selected_format': {},
            'adverbs': ('always', 'never')
        }

        desc_map = {}

        for i in self.subscribers:
            if not i.description():
                continue
            if not req.session.authenticated and i.requires_authentication():
                continue
            data['subscribers'].append({
                'class': i.__class__.__name__,
                'description': i.description()
            })
            desc_map[i.__class__.__name__] = i.description()

        for i in self.distributors:
            for j in i.transports():
                data['rules'][j] = []
                for r in Subscription.find_by_sid_and_distributor(
                        self.env, req.session.sid, req.session.authenticated,
                        j):
                    if desc_map.get(r['class']):
                        data['rules'][j].append({
                            'id':
                            r['id'],
                            'adverb':
                            r['adverb'],
                            'description':
                            desc_map[r['class']],
                            'priority':
                            r['priority']
                        })
                        data['selected_format'][j] = r['format']

        data['default_rules'] = {}
        defaults = []
        for i in self.default_subscribers:
            defaults.extend(i.default_subscriptions())

        for r in sorted(defaults, key=operator.itemgetter(2)):
            klass, dist, _, adverb = r
            if not data['default_rules'].get(dist):
                data['default_rules'][dist] = []
            if desc_map.get(klass):
                data['default_rules'][dist].append({
                    'adverb':
                    adverb,
                    'description':
                    desc_map.get(klass)
                })

        add_stylesheet(req, 'announcer/css/announcer_prefs.css')
        if hasattr(Chrome(self.env), 'jenv'):
            return 'prefs_announcer_manage_subscriptions.html', dict(
                data=data), None
        else:
            return 'prefs_announcer_manage_subscriptions.html', dict(data=data)

    # ITemplateStreamFilter method

    def filter_stream(self, req, method, filename, stream, data):
        if re.match(r'/prefs/subscription', req.path_info):
            xpath_match = '//form[@id="userprefs"]//div[@class="buttons"]'
            stream |= Transformer(xpath_match).empty()
        return stream

    def _add_rule(self, arg, req):
        rule = Subscription(self.env)
        rule['sid'] = req.session.sid
        rule['authenticated'] = req.session.authenticated and 1 or 0
        rule['distributor'] = arg
        rule['format'] = req.args.get('format-%s' % arg, '')
        rule['adverb'] = req.args['new-adverb-%s' % arg]
        rule['class'] = req.args['new-rule-%s' % arg]
        Subscription.add(self.env, rule)

    def _delete_rule(self, arg, req):
        Subscription.delete(self.env, arg)

    def _move_rule(self, arg, req):
        (rule_id, new_priority) = arg.split('-')
        if int(new_priority) >= 1:
            Subscription.move(self.env, rule_id, int(new_priority))

    def _set_format(self, arg, req):
        Subscription.update_format_by_distributor_and_sid(
            self.env, arg, req.session.sid, req.session.authenticated,
            req.args['format-%s' % arg])
Ejemplo n.º 22
0
class AdvancedSearchPlugin(Component):
	implements(
		INavigationContributor,
		IPermissionRequestor,
		IRequestHandler,
		ITemplateProvider,
		ITicketChangeListener,
		IWikiChangeListener,
		IWikiSyntaxProvider,
	)

	providers = ExtensionPoint(IAdvSearchBackend)

	DEFAULT_PER_PAGE = 10

	def _get_source_filters(self):
		return set(itertools.chain(*(p.get_sources() for p in self.providers)))

	# INavigationContributor methods
	def get_active_navigation_item(self, req):
		return 'advsearch'

	def get_navigation_items(self, req):
		if 'SEARCH_VIEW' in req.perm:
			label = self.config.get(
				'advanced_search_plugin',
				'menu_label',
				'Advanced Search'
			)
			yield ('mainnav',
				'advsearch',
				html.A(_(label), href=self.env.href.advsearch())
			)

	# IPermissionRequestor methods
	def get_permission_actions(self):
		return ['SEARCH_VIEW']

	# IRequestHandler methods
	def match_request(self, req):
		# TODO: add /search if search module is disabled
		return re.match(r'/advsearch?', req.path_info) is not None

	def process_request(self, req):
		"""
		Implements IRequestHandler.process_request

		Build a dict of search criteria from the user and request results from
		the active AdvancedSearchBackend.
		"""
		req.perm.assert_permission('SEARCH_VIEW')

		try:
			per_page = int(req.args.getfirst('per_page',
				self.DEFAULT_PER_PAGE))
		except ValueError:
			self.log.warn('Could not set per_page to %s' %
					req.args.getfirst('per_page'))
			per_page = self.DEFAULT_PER_PAGE

		try:
			page = int(req.args.getfirst('page', 1))
		except ValueError:
			page = 1

		data = {
			'source': self._get_filter_dicts(req.args),
			'author': [auth for auth in req.args.getlist('author') if auth],
			'date_start': req.args.getfirst('date_start'),
			'date_end': req.args.getfirst('date_end'),
			'q': req.args.get('q'),
			'start_points': StartPoints.parse_args(req.args, self.providers),
			'per_page': per_page,
			'ticket_statuses': self._get_ticket_statuses(req.args),
			'source_filter_defaults': self.config.get('advanced_search_plugin', 'source_filter_defaults', '').split(','),
		}

		# Initial page request
		if not any((data['q'], data['author'], data['date_start'], data['date_end'])):
			return self._send_response(req, data)

		# Look for quickjump
		quickjump = self._get_quickjump(req, data['q'])
		if quickjump:
			req.redirect(quickjump)

		# perform query using backend if q is set
		result_map = {}
		total_count = 0
		for provider in self.providers:
			result_count, result_list = 0, []
			try:
				result_count, result_list = provider.query_backend(data)
			except SearchBackendException, e:
				add_warning(req, _('SearchBackendException: %s' % e))
			total_count += result_count
			result_map[provider.get_name()] = result_list

		if not total_count:
			return self._send_response(req, data)

		data['page'] = page
		results = self._merge_results(result_map, per_page)
		self._add_href_to_results(results)
		data['results'] = Paginator(
			results,
			page=page-1,
			max_per_page=per_page,
			num_items=total_count
		)

		# pagination next/prev links
		if data['results'].has_next_page:
			data['start_points'] = StartPoints.format(results, data['start_points'])

		return self._send_response(req, data)
Ejemplo n.º 23
0
class Environment(Component, ComponentManager):
    """Trac environment manager.

    Trac stores project information in a Trac environment. It consists of a
    directory structure containing among other things:
     * a configuration file
     * an SQLite database (stores tickets, wiki pages...)
     * project-specific templates and plugins
     * wiki and ticket attachments
    """
    implements(ISystemInfoProvider)

    required = True

    system_info_providers = ExtensionPoint(ISystemInfoProvider)
    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)

    shared_plugins_dir = PathOption(
        'inherit', 'plugins_dir', '',
        """Path to the //shared plugins directory//.
        
        Plugins in that directory are loaded in addition to those in the
        directory of the environment `plugins`, with this one taking 
        precedence.
        
        (''since 0.11'')""")

    base_url = Option(
        'trac', 'base_url', '', """Reference URL for the Trac deployment.
        
        This is the base URL that will be used when producing documents that
        will be used outside of the web browsing context, like for example
        when inserting URLs pointing to Trac resources in notification
        e-mails.""")

    base_url_for_redirect = BoolOption(
        'trac', 'use_base_url_for_redirect', False,
        """Optionally use `[trac] base_url` for redirects.
        
        In some configurations, usually involving running Trac behind a HTTP
        proxy, Trac can't automatically reconstruct the URL that is used to
        access it. You may need to use this option to force Trac to use the
        `base_url` setting also for redirects. This introduces the obvious
        limitation that this environment will only be usable when accessible
        from that URL, as redirects are frequently used. ''(since 0.10.5)''""")

    secure_cookies = BoolOption(
        'trac', 'secure_cookies', False,
        """Restrict cookies to HTTPS connections.
        
        When true, set the `secure` flag on all cookies so that they are
        only sent to the server on HTTPS connections. Use this if your Trac
        instance is only accessible through HTTPS. (''since 0.11.2'')""")

    project_name = Option('project', 'name', 'My Project',
                          """Name of the project.""")

    project_description = Option('project', 'descr', 'My example project',
                                 """Short description of the project.""")

    project_url = Option(
        'project', 'url', '',
        """URL of the main project web site, usually the website in which
        the `base_url` resides. This is used in notification e-mails.""")

    project_admin = Option(
        'project', 'admin', '',
        """E-Mail address of the project's administrator.""")

    project_admin_trac_url = Option(
        'project', 'admin_trac_url', '.',
        """Base URL of a Trac instance where errors in this Trac should be
        reported.
        
        This can be an absolute or relative URL, or '.' to reference this
        Trac instance. An empty value will disable the reporting buttons.
        (''since 0.11.3'')""")

    project_footer = Option(
        'project', 'footer',
        N_('Visit the Trac open source project at<br />'
           '<a href="http://trac.edgewall.org/">'
           'http://trac.edgewall.org/</a>'),
        """Page footer text (right-aligned).""")

    project_icon = Option('project', 'icon', 'common/trac.ico',
                          """URL of the icon of the project.""")

    log_type = Option(
        'logging', 'log_type', 'none', """Logging facility to use.
        
        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")

    log_file = Option(
        'logging', 'log_file', 'trac.log',
        """If `log_type` is `file`, this should be a path to the log-file.
        Relative paths are resolved relative to the `log` directory of the
        environment.""")

    log_level = Option(
        'logging', 'log_level', 'DEBUG', """Level of verbosity in log.
        
        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")

    log_format = Option(
        'logging', 'log_format', None, """Custom logging format.

        If nothing is set, the following will be used:
        
        Trac[$(module)s] $(levelname)s: $(message)s

        In addition to regular key names supported by the Python logger library
        (see http://docs.python.org/library/logging.html), one could use:
         - $(path)s     the path for the current environment
         - $(basename)s the last path component of the current environment
         - $(project)s  the project name

        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
        would be interpreted by the ConfigParser itself.

        Example:
        `($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s`

        ''(since 0.10.5)''""")

    def __init__(self, path, create=False, options=[]):
        """Initialize the Trac environment.
        
        @param path:   the absolute path to the Trac environment
        @param create: if `True`, the environment is created and populated with
                       default data; otherwise, the environment is expected to
                       already exist.
        @param options: A list of `(section, name, value)` tuples that define
                        configuration options
        """
        ComponentManager.__init__(self)

        self.path = path
        self.systeminfo = []
        self._href = self._abs_href = None

        if create:
            self.create(options)
        else:
            self.verify()
            self.setup_config()

        if create:
            for setup_participant in self.setup_participants:
                setup_participant.environment_created()

    def get_systeminfo(self):
        """Return a list of `(name, version)` tuples describing the name and
        version information of external packages used by Trac and plugins.
        """
        info = self.systeminfo[:]
        for provider in self.system_info_providers:
            info.extend(provider.get_system_info() or [])
        info.sort(key=lambda (name, version): (name != 'Trac', name.lower()))
        return info

    # ISystemInfoProvider methods

    def get_system_info(self):
        from trac import core, __version__ as VERSION
        yield 'Trac', get_pkginfo(core).get('version', VERSION)
        yield 'Python', sys.version
        yield 'setuptools', setuptools.__version__
        from trac.util.datefmt import pytz
        if pytz is not None:
            yield 'pytz', pytz.__version__

    def component_activated(self, component):
        """Initialize additional member variables for components.
        
        Every component activated through the `Environment` object gets three
        member variables: `env` (the environment object), `config` (the
        environment configuration) and `log` (a logger object)."""
        component.env = self
        component.config = self.config
        component.log = self.log

    def _component_name(self, name_or_class):
        name = name_or_class
        if not isinstance(name_or_class, basestring):
            name = name_or_class.__module__ + '.' + name_or_class.__name__
        return name.lower()

    @property
    def _component_rules(self):
        try:
            return self._rules
        except AttributeError:
            self._rules = {}
            for name, value in self.config.options('components'):
                if name.endswith('.*'):
                    name = name[:-2]
                self._rules[name.lower()] = value.lower() in ('enabled', 'on')
            return self._rules

    def is_component_enabled(self, cls):
        """Implemented to only allow activation of components that are not
        disabled in the configuration.
        
        This is called by the `ComponentManager` base class when a component is
        about to be activated. If this method returns `False`, the component
        does not get activated. If it returns `None`, the component only gets
        activated if it is located in the `plugins` directory of the
        enironment.
        """
        component_name = self._component_name(cls)

        # Disable the pre-0.11 WebAdmin plugin
        # Please note that there's no recommendation to uninstall the
        # plugin because doing so would obviously break the backwards
        # compatibility that the new integration administration
        # interface tries to provide for old WebAdmin extensions
        if component_name.startswith('webadmin.'):
            self.log.info('The legacy TracWebAdmin plugin has been '
                          'automatically disabled, and the integrated '
                          'administration interface will be used '
                          'instead.')
            return False

        rules = self._component_rules
        cname = component_name
        while cname:
            enabled = rules.get(cname)
            if enabled is not None:
                return enabled
            idx = cname.rfind('.')
            if idx < 0:
                break
            cname = cname[:idx]

        # By default, all components in the trac package are enabled
        return component_name.startswith('trac.') or None

    def enable_component(self, cls):
        """Enable a component or module."""
        self._component_rules[self._component_name(cls)] = True

    def verify(self):
        """Verify that the provided path points to a valid Trac environment
        directory."""
        fd = open(os.path.join(self.path, 'VERSION'), 'r')
        try:
            assert fd.read(26) == 'Trac Environment Version 1'
        finally:
            fd.close()

    def get_db_cnx(self):
        """Return a database connection from the connection pool (deprecated)

        Use `with_transaction` for obtaining a writable database connection
        and `get_read_db` for anything else.
        """
        return get_read_db(self)

    def with_transaction(self, db=None):
        """Decorator for transaction functions.

        See `trac.db.api.with_transaction` for detailed documentation."""
        return with_transaction(self, db)

    def get_read_db(self):
        """Return a database connection for read purposes.

        See `trac.db.api.get_read_db` for detailed documentation."""
        return get_read_db(self)

    def shutdown(self, tid=None):
        """Close the environment."""
        RepositoryManager(self).shutdown(tid)
        DatabaseManager(self).shutdown(tid)
        if tid is None:
            self.log.removeHandler(self._log_handler)
            self._log_handler.flush()
            self._log_handler.close()
            del self._log_handler

    def get_repository(self, reponame=None, authname=None):
        """Return the version control repository with the given name, or the
        default repository if `None`.
        
        The standard way of retrieving repositories is to use the methods
        of `RepositoryManager`. This method is retained here for backward
        compatibility.
        
        @param reponame: the name of the repository
        @param authname: the user name for authorization (not used anymore,
                         left here for compatibility with 0.11)
        """
        return RepositoryManager(self).get_repository(reponame)

    def create(self, options=[]):
        """Create the basic directory structure of the environment, initialize
        the database and populate the configuration file with default values.

        If options contains ('inherit', 'file'), default values will not be
        loaded; they are expected to be provided by that file or other options.
        """
        # Create the directory structure
        if not os.path.exists(self.path):
            os.mkdir(self.path)
        os.mkdir(self.get_log_dir())
        os.mkdir(self.get_htdocs_dir())
        os.mkdir(os.path.join(self.path, 'plugins'))

        # Create a few files
        create_file(os.path.join(self.path, 'VERSION'),
                    'Trac Environment Version 1\n')
        create_file(
            os.path.join(self.path, 'README'),
            'This directory contains a Trac environment.\n'
            'Visit http://trac.edgewall.org/ for more information.\n')

        # Setup the default configuration
        os.mkdir(os.path.join(self.path, 'conf'))
        create_file(os.path.join(self.path, 'conf', 'trac.ini.sample'))
        config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
        for section, name, value in options:
            config.set(section, name, value)
        config.save()
        self.setup_config()
        if not any((section, option) == ('inherit', 'file')
                   for section, option, value in options):
            self.config.set_defaults(self)
            self.config.save()

        # Create the database
        DatabaseManager(self).init_db()

    def get_version(self, db=None, initial=False):
        """Return the current version of the database.
        If the optional argument `initial` is set to `True`, the version
        of the database used at the time of creation will be returned.

        In practice, for database created before 0.11, this will return `False`
        which is "older" than any db version number.

        :since 0.11:
        """
        if not db:
            db = self.get_db_cnx()
        cursor = db.cursor()
        cursor.execute("SELECT value FROM system "
                       "WHERE name='%sdatabase_version'" %
                       (initial and 'initial_' or ''))
        row = cursor.fetchone()
        return row and int(row[0])

    def setup_config(self):
        """Load the configuration file."""
        self.config = Configuration(os.path.join(self.path, 'conf',
                                                 'trac.ini'))
        self.setup_log()
        from trac.loader import load_components
        plugins_dir = self.shared_plugins_dir
        load_components(self, plugins_dir and (plugins_dir, ))

    def get_templates_dir(self):
        """Return absolute path to the templates directory."""
        return os.path.join(self.path, 'templates')

    def get_htdocs_dir(self):
        """Return absolute path to the htdocs directory."""
        return os.path.join(self.path, 'htdocs')

    def get_log_dir(self):
        """Return absolute path to the log directory."""
        return os.path.join(self.path, 'log')

    def setup_log(self):
        """Initialize the logging sub-system."""
        from trac.log import logger_handler_factory
        logtype = self.log_type
        logfile = self.log_file
        if logtype == 'file' and not os.path.isabs(logfile):
            logfile = os.path.join(self.get_log_dir(), logfile)
        format = self.log_format
        if format:
            format = format.replace('$(', '%(') \
                     .replace('%(path)s', self.path) \
                     .replace('%(basename)s', os.path.basename(self.path)) \
                     .replace('%(project)s', self.project_name)
        self.log, self._log_handler = logger_handler_factory(logtype,
                                                             logfile,
                                                             self.log_level,
                                                             self.path,
                                                             format=format)
        from trac import core, __version__ as VERSION
        self.log.info('-' * 32 + ' environment startup [Trac %s] ' + '-' * 32,
                      get_pkginfo(core).get('version', VERSION))

    def get_known_users(self, cnx=None):
        """Generator that yields information about all known users, i.e. users
        that have logged in to this Trac environment and possibly set their name
        and email.

        This function generates one tuple for every user, of the form
        (username, name, email) ordered alpha-numerically by username.

        @param cnx: the database connection; if ommitted, a new connection is
                    retrieved
        """
        if not cnx:
            cnx = self.get_db_cnx()
        cursor = cnx.cursor()
        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
                       "FROM session AS s "
                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
                       "  and n.authenticated=1 AND n.name = 'name') "
                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
                       "  AND e.authenticated=1 AND e.name = 'email') "
                       "WHERE s.authenticated=1 ORDER BY s.sid")
        for username, name, email in cursor:
            yield username, name, email

    def backup(self, dest=None):
        """Create a backup of the database.

        @param dest: Destination file; if not specified, the backup is stored in
                     a file called db_name.trac_version.bak
        """
        return DatabaseManager(self).backup(dest)

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        db = self.get_db_cnx()
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                self.log.warning('Component %s requires environment upgrade',
                                 participant)
                return True
        return False

    def upgrade(self, backup=False, backup_dest=None):
        """Upgrade database.
        
        @param backup: whether or not to backup before upgrading
        @param backup_dest: name of the backup file
        @return: whether the upgrade was performed
        """
        upgraders = []
        db = self.get_read_db()
        for participant in self.setup_participants:
            if participant.environment_needs_upgrade(db):
                upgraders.append(participant)
        if not upgraders:
            return

        if backup:
            self.backup(backup_dest)

        for participant in upgraders:
            self.log.info("%s.%s upgrading...", participant.__module__,
                          participant.__class__.__name__)
            with_transaction(self)(participant.upgrade_environment)
            # Database schema may have changed, so close all connections
            DatabaseManager(self).shutdown()
        return True

    def _get_href(self):
        if not self._href:
            self._href = Href(urlsplit(self.abs_href.base)[2])
        return self._href

    href = property(_get_href, 'The application root path')

    def _get_abs_href(self):
        if not self._abs_href:
            if not self.base_url:
                self.log.warn('base_url option not set in configuration, '
                              'generated links may be incorrect')
                self._abs_href = Href('')
            else:
                self._abs_href = Href(self.base_url)
        return self._abs_href

    abs_href = property(_get_abs_href, 'The application URL')
Ejemplo n.º 24
0
class Daemon(Component):
    implements(IRequestHandler, IAdminPanelProvider, ITemplateProvider)

    jobs = ExtensionPoint(ICronJob)
    engine = None
    entries = {}

    def __init__(self):
        Component.__init__(self)
        self.entries = self.get_entries()
        self.__class__.engine = self
        t = threading.Thread(target=self.poll)
        t.daemon = True
        t.start()

    # NOTE:
    #  I cannot use compmgr.is_enabled() because in case that the egg in project/plugin folder
    #  always enable even if no configuration line in [component] section.
    #  -> mandatory adding the configuration line in trac.ini
    # NOTE:
    #  I cannot use compmgr.is_enabled() because in case that execute with tracd-script
    #  and disabled in configuration always the method returns true.
    #  I have to see the configuration not via compmgr.
    def poll(self):
        key = '%s.%s' % (self.__module__, self.__class__.__name__)
        while True:
            if not self.config.getbool('components', key):
                self.env.log.debug("cron engine has disabled ... exiting.")
                break  # exit when disabled
            if self != self.__class__.engine:
                self.env.log.debug(
                    "cron engine rebooted ... old thread is exiting.")
                break  # exit when another thread is created
            # runs every interval in seconds
            now = datetime.now()
            for job in self.jobs:
                # decide execute the job or not
                entry = self.entries[getkey(job)]
                if entry and \
                        now.minute in entry[0] and \
                        now.hour in entry[1] and \
                        now.month in entry[3] and \
                        (now.day in entry[2] or \
                            (now.weekday() + 1) % 7 in entry[4]):
                    try:
                        job.run()
                    except:
                        self.env.log.exception("Unexpected error:")
                else:
                    #                    self.env.log.debug('Skipped: ' + job.__repr__())
                    pass
            time.sleep(10 - datetime.now().second % 10)
        pass  # thread end
        self.env.log.debug("exit from polling loop. " + self.__repr__())

    def parse_entry(self, arg):  # str is not contains space char
        # throws ValueError if int(str) fails
        if arg == '*':  # "*"
            return ANY
        result = []
        params = arg.split(
            ',')  # "1,2,3-5,6-10/2" -> ["1", "2", "3-5", "6-10/2"]
        for param in params:
            if not "-" in param:
                result.append(int(param))  # "1", "2" -> [ ..., 1, 2]
                continue
            param = param.split(
                '-', 1)  # "3-5" -> ["3", "5"]  # "6-10/2" -> ["6", "10/2"]
            if not '/' in param[1]:
                result.extend(range(int(param[0]),
                                    int(param[1]) + 1))  # [ ..., 3, 4, 5]
                continue
            param[1], step = param[1].split('/', 1)
            result.extend(range(int(param[0]),
                                int(param[1]) + 1,
                                int(step)))  # [ ..., 6, 8, 10]
        return result

    def load_entry(self, entry):
        # http://svn.freebsd.org/base/head/usr.sbin/cron/lib/entry.c
        entry = entry.strip()
        if entry.startswith('#'):
            return [NEVER, NEVER, NEVER, NEVER, NEVER]
        elif entry.startswith('@reboot'):
            return False  # Not Implemented
        elif entry.startswith('@yearly'):
            return [ZERO, ZERO, ONE, ONE, ANY]
        elif entry.startswith('@annually'):
            return [ZERO, ZERO, ONE, ONE, ANY]
        elif entry.startswith('@monthly'):
            return [ZERO, ZERO, ONE, ANY, ANY]
        elif entry.startswith('@weekly'):
            return [ZERO, ZERO, ANY, ANY, ZERO]
        elif entry.startswith('@daily'):
            return [ZERO, ZERO, ANY, ANY, ANY]
        elif entry.startswith('@midnight'):
            return [ZERO, ZERO, ANY, ANY, ANY]
        elif entry.startswith('@hourly'):
            return [ZERO, ANY, ANY, ANY, ANY]
        elif entry.startswith('@every_minute'):
            return [ANY, ANY, ANY, ANY, ANY]
        elif entry.startswith('@every_second'):
            return False  # Not Implemented
        else:  # about to parse numerics
            entries = entry.split(' ')
            if len(entries) != 5:
                return False  # Not Implemented
            try:
                return map(self.parse_entry, entries)
            except:  # ValueError
                return False

    def get_entries(self):
        result = {}
        for job in self.jobs:
            key = getkey(job)
            time_field = self.config.get(section, key, False) or \
                (job.default_time() if 'default_time' in dir(job) else '# 0 * * * *')
            result[key] = self.load_entry(time_field)
        return result

    # IRequestHandler Methods
    def match_request(self, req):
        return False

    def process_request(self, req):
        pass

    # IAdminPanelProvider methods
    def get_admin_panels(self, req):
        if req.perm.has_permission('TRAC_ADMIN'):
            yield ('cron', 'Cron', 'crontab', 'Crontab')

    def render_admin_panel(self, req, category, page, path_info):
        req.perm.require('TICKET_ADMIN')
        if req.method == 'POST':
            darty = False
            for key in req.args.keys():
                if key.startswith('cron_'):
                    entry_string = req.args.get(key)
                    entry = self.load_entry(entry_string)
                    if entry:
                        self.entries[key[5:]] = entry
                        self.config.set(section, key[5:], entry_string)
                        darty = True
                    else:
                        # notify.append({key[5:], entry_string})
                        add_warning(
                            req, "cycle \"%s\" cannot recognized for %s. " %
                            (entry_string, key[5:]))
                        pass
            if darty and len(req.chrome['warnings']) == 0:
                self.config.save()
                req.redirect(req.href.admin(category, page))
        data = []
        for job in self.jobs:
            key = getkey(job)
            entry_string = req.args.get('cron_' + key) or \
                self.config.get(section, key, False) or \
                (job.default_time() if 'default_time' in dir(job) else '# 0 * * * *')
            data.append({'name': key, 'cycle': entry_string})
        return 'crontab.html', {'jobs': data}

    # ITemplateProvider methods
    def get_templates_dirs(self):
        return [ResourceManager().resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        return []
Ejemplo n.º 25
0
class NotificationPreferences(Component):
    implements(IPreferencePanelProvider, ITemplateProvider)

    subscribers = ExtensionPoint(INotificationSubscriber)
    distributors = ExtensionPoint(INotificationDistributor)
    formatters = ExtensionPoint(INotificationFormatter)

    def __init__(self):
        self.post_handlers = {
            'add-rule': self._add_rule,
            'delete-rule': self._delete_rule,
            'move-rule': self._move_rule,
            'replace': self._replace_rules,
        }

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        yield ('notification', _('Notifications'))

    def render_preference_panel(self, req, panel, path_info=None):
        if req.method == 'POST':
            action_arg = req.args.getfirst('action', '').split('_', 1)
            if len(action_arg) == 2:
                action, arg = action_arg
                handler = self.post_handlers.get(action)
                if handler:
                    handler(arg, req)
                    add_notice(req, _("Your preferences have been saved."))
            req.redirect(req.href.prefs('notification'))

        rules = {}
        subscribers = []
        formatters = {}
        selected_format = {}
        defaults = []

        for i in self.subscribers:
            description = i.description()
            if not description:
                continue
            if not req.session.authenticated and i.requires_authentication():
                continue
            subscribers.append({
                'class': i.__class__.__name__,
                'description': description
            })
            if hasattr(i, 'default_subscriptions'):
                defaults.extend(i.default_subscriptions())
        desc_map = dict((s['class'], s['description']) for s in subscribers)

        for t in self._iter_transports():
            rules[t] = []
            formatters[t] = self._get_supported_styles(t)
            selected_format[t] = req.session.get('notification.format.%s' % t)
            for r in self._iter_rules(req, t):
                description = desc_map.get(r['class'])
                if description:
                    values = {'description': description}
                    values.update(
                        (key, r[key])
                        for key in ('id', 'adverb', 'class', 'priority'))
                    rules[t].append(values)

        default_rules = {}
        for r in sorted(defaults, key=itemgetter(3)):  # sort by priority
            klass, dist, format, priority, adverb = r
            default_rules.setdefault(dist, [])
            description = desc_map.get(klass)
            if description:
                default_rules[dist].append({
                    'adverb': adverb,
                    'description': description
                })

        data = {
            'rules': rules,
            'subscribers': subscribers,
            'formatters': formatters,
            'selected_format': selected_format,
            'default_rules': default_rules,
            'adverbs': ('always', 'never'),
            'adverb_labels': {
                'always': _("Notify"),
                'never': _("Never notify")
            }
        }
        Chrome(self.env).add_jquery_ui(req)
        return 'prefs_notification.html', dict(data=data)

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        resource_dir = resource_filename('trac.notification', 'templates')
        return [resource_dir]

    # Internal methods

    def _add_rule(self, arg, req):
        rule = Subscription(self.env)
        rule['sid'] = req.session.sid
        rule['authenticated'] = 1 if req.session.authenticated else 0
        rule['distributor'] = arg
        rule['format'] = req.args.get('format-%s' % arg, '')
        rule['adverb'] = req.args['new-adverb-%s' % arg]
        rule['class'] = req.args['new-rule-%s' % arg]
        Subscription.add(self.env, rule)

    def _delete_rule(self, arg, req):
        session = req.session
        Subscription.delete(self.env, arg, session.sid, session.authenticated)

    def _move_rule(self, arg, req):
        tokens = [as_int(val, 0) for val in arg.split('-', 1)]
        if len(tokens) == 2:
            rule_id, priority = tokens
            if rule_id > 0 and priority > 0:
                session = req.session
                Subscription.move(self.env, rule_id, priority, session.sid,
                                  session.authenticated)

    def _replace_rules(self, arg, req):
        subscriptions = []
        for transport in self._iter_transports():
            format_ = req.args.getfirst('format-' + transport)
            format_ = self._normalize_format(format_, transport)
            req.session.set('notification.format.%s' % transport, format_, '')
            adverbs = req.args.getlist('adverb-' + transport)
            classes = req.args.getlist('class-' + transport)
            for idx in xrange(min(len(adverbs), len(classes))):
                subscriptions.append({
                    'distributor': transport,
                    'format': format_,
                    'adverb': adverbs[idx],
                    'class': classes[idx]
                })

        sid = req.session.sid
        authenticated = req.session.authenticated
        with self.env.db_transaction:
            Subscription.replace_all(self.env, sid, authenticated,
                                     subscriptions)

    def _iter_rules(self, req, transport):
        session = req.session
        for r in Subscription.find_by_sid_and_distributor(
                self.env, session.sid, session.authenticated, transport):
            yield r

    def _iter_transports(self):
        for distributor in self.distributors:
            for transport in distributor.transports():
                yield transport

    def _get_supported_styles(self, transport):
        styles = set()
        for formatter in self.formatters:
            for style, realm_ in formatter.get_supported_styles(transport):
                styles.add(style)
        return sorted(styles)

    def _normalize_format(self, format_, transport):
        if format_:
            styles = self._get_supported_styles(transport)
            if format_ in styles:
                return format_
        return ''
Ejemplo n.º 26
0
class RelationsSystem(Component):
    PARENT_RELATION_TYPE = 'parent'
    CHILDREN_RELATION_TYPE = 'children'

    changing_listeners = ExtensionPoint(IRelationChangingListener)
    all_validators = ExtensionPoint(IRelationValidator)
    global_validators = OrderedExtensionsOption(
        'bhrelations',
        'global_validators',
        IRelationValidator,
        'NoSelfReferenceValidator, ExclusiveValidator, BlockerValidator',
        include_missing=False,
        doc="""Validators used to validate all relations,
        regardless of their type.""",
        doc_domain='bhrelations')

    duplicate_relation_type = Option(
        'bhrelations',
        'duplicate_relation',
        'duplicateof',
        "Relation type to be used with the resolve as duplicate workflow.",
        doc_domain='bhrelations')

    def __init__(self):
        import pkg_resources
        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
        add_domain(self.env.path, locale_dir)
        links, labels, validators, blockers, copy_fields, exclusive = \
            self._parse_config()
        self._links = links
        self._labels = labels
        self._validators = validators
        self._blockers = blockers
        self._copy_fields = copy_fields
        self._exclusive = exclusive

        self.link_ends_map = {}
        for end1, end2 in self.get_ends():
            self.link_ends_map[end1] = end2
            if end2 is not None:
                self.link_ends_map[end2] = end1

    def get_ends(self):
        return self._links

    def add(self,
            source_resource_instance,
            destination_resource_instance,
            relation_type,
            comment=None,
            author=None,
            when=None):
        source = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, source_resource_instance)
        destination = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, destination_resource_instance)
        if relation_type not in self.link_ends_map:
            raise UnknownRelationType(relation_type)
        if when is None:
            when = datetime.now(utc)
        relation = Relation(self.env)
        relation.source = source
        relation.destination = destination
        relation.type = relation_type
        relation.comment = comment
        relation.author = author
        relation.when = when
        self.add_relation(relation)
        return relation

    def get_reverted_relation(self, relation):
        """Return None if relation is one way"""
        other_end = self.link_ends_map[relation.type]
        if other_end:
            return relation.clone_reverted(other_end)

    def add_relation(self, relation):
        self.validate(relation)
        with self.env.db_transaction:
            relation.insert()
            reverted_relation = self.get_reverted_relation(relation)
            if reverted_relation:
                reverted_relation.insert()

            for listener in self.changing_listeners:
                listener.adding_relation(relation)

    def delete(self, relation_id, when=None):
        if when is None:
            when = datetime.now(utc)
        relation = Relation.load_by_relation_id(self.env, relation_id)
        source = relation.source
        destination = relation.destination
        relation_type = relation.type
        with self.env.db_transaction:
            cloned_relation = relation.clone()
            relation.delete()
            other_end = self.link_ends_map[relation_type]
            if other_end:
                reverted_relation = Relation(self.env,
                                             keys=dict(
                                                 source=destination,
                                                 destination=source,
                                                 type=other_end,
                                             ))
                reverted_relation.delete()

            for listener in self.changing_listeners:
                listener.deleting_relation(cloned_relation, when)

            from bhrelations.notification import RelationNotifyEmail
            RelationNotifyEmail(self.env).notify(cloned_relation, deleted=when)

    def delete_resource_relations(self, resource_instance):
        sql = "DELETE FROM " + Relation.get_table_name() + \
              " WHERE source=%s OR destination=%s"
        full_resource_id = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, resource_instance)
        with self.env.db_transaction as db:
            db(sql, (full_resource_id, full_resource_id))

    def _debug_select(self):
        """The method is used for debug purposes"""
        sql = "SELECT id, source, destination, type FROM bloodhound_relations"
        with self.env.db_query as db:
            return [db(sql)]

    def get_relations(self, resource_instance):
        relation_list = []
        for relation in self._select_relations_for_resource_instance(
                resource_instance):
            relation_list.append(
                dict(
                    relation_id=relation.get_relation_id(),
                    destination_id=relation.destination,
                    destination=ResourceIdSerializer.get_resource_by_id(
                        relation.destination),
                    type=relation.type,
                    comment=relation.comment,
                    when=relation.when,
                    author=relation.author,
                ))
        return relation_list

    def _select_relations_for_resource_instance(self, resource):
        resource_full_id = ResourceIdSerializer.get_resource_id_from_instance(
            self.env, resource)
        return self._select_relations(resource_full_id)

    def _select_relations(self,
                          source=None,
                          resource_type=None,
                          destination=None):
        #todo: add optional paging for possible umbrella tickets with
        #a lot of child tickets
        where = dict()
        if source:
            where["source"] = source
        if resource_type:
            where["type"] = resource_type
            order_by = ["destination"]
        else:
            order_by = ["type", "destination"]
        if destination:
            where["destination"] = destination
        return Relation.select(self.env, where=where, order_by=order_by)

    def _parse_config(self):
        links = []
        labels = {}
        validators = {}
        blockers = {}
        copy_fields = {}
        exclusive = set()

        config = self.config[RELATIONS_CONFIG_NAME]
        for name in [
                option for option, _ in config.options() if '.' not in option
        ]:
            reltypes = config.getlist(name)
            if not reltypes:
                continue
            if len(reltypes) == 1:
                reltypes += [None]
            links.append(tuple(reltypes))

            custom_validators = self._parse_validators(config, name)
            for rel in filter(None, reltypes):
                labels[rel] = \
                    config.get(rel + '.label') or rel.capitalize()
                blockers[rel] = \
                    config.getbool(rel + '.blocks', default=False)
                if config.getbool(rel + '.exclusive'):
                    exclusive.add(rel)
                validators[rel] = custom_validators

                # <end>.copy_fields may be absent or intentionally set empty.
                # config.getlist() will return [] in either case, so check that
                # the key is present before assigning the value
                cf_key = '%s.copy_fields' % rel
                if cf_key in config:
                    copy_fields[rel] = config.getlist(cf_key)
        return links, labels, validators, blockers, copy_fields, exclusive

    def _parse_validators(self, section, name):
        custom_validators = set('%sValidator' % validator for validator in set(
            section.getlist(name + '.validators', [], ',', True)))
        validators = []
        if custom_validators:
            for impl in self.all_validators:
                if impl.__class__.__name__ in custom_validators:
                    validators.append(impl)
        return validators

    def validate(self, relation):
        """
        Validate the relation using the configured validators. Validation is
        always run on the relation with master type.
        """
        backrel = self.get_reverted_relation(relation)
        if backrel and (backrel.type, relation.type) in self._links:
            relation = backrel

        for validator in self.global_validators:
            validator.validate(relation)

        for validator in self._validators.get(relation.type, ()):
            validator.validate(relation)

    def is_blocker(self, relation_type):
        return self._blockers[relation_type]

    def render_relation_type(self, end):
        return self._labels[end]

    def get_relation_types(self):
        return self._labels

    def find_blockers(self, resource_instance, is_blocker_method):
        # tbd: do we blocker finding to be recursive
        all_blockers = []
        for relation in self._select_relations_for_resource_instance(
                resource_instance):
            if self.is_blocker(relation.type):
                resource = ResourceIdSerializer.get_resource_by_id(
                    relation.destination)
                resource_instance = is_blocker_method(resource)
                if resource_instance is not None:
                    all_blockers.append(resource_instance)
                    # blockers = self._recursive_find_blockers(
                    #     relation, is_blocker_method)
                    # if blockers:
                    #     all_blockers.extend(blockers)
        return all_blockers

    def get_resource_name(self, resource_id):
        resource = ResourceIdSerializer.get_resource_by_id(resource_id)
        return get_resource_shortname(self.env, resource)
Ejemplo n.º 27
0
 def __init__(self, section, name, interface, default=None, doc='',
              doc_domain='tracini'):
     Option.__init__(self, section, name, default, doc, doc_domain)
     self.xtnpt = ExtensionPoint(interface)
Ejemplo n.º 28
0
class OidcPlugin(Component):
    """ Authenticate via OpenID Connect

    """
    implements(INavigationContributor, IRequestHandler)

    RETURN_URL_SKEY = 'trac_oidc.return_url'

    client_secret_file = PathOption(
        'trac_oidc', 'client_secret_file', 'client_secret.json',
        """Path to client_secret file.  Relative paths are interpreted
        relative to the ``conf`` subdirectory of the trac environment.""")

    # deprecated
    absolute_trust_root = BoolOption(
        'openid', 'absolute_trust_root', 'true',
        """Whether we should use absolute trust root or by project.""")

    login_managers = ExtensionPoint(ILoginManager)

    def __init__(self):
        # We should show our own "Logout" link only if the stock
        # LoginModule is disabled.
        self.show_logout_link = not is_component_enabled(self.env, LoginModule)

        self.userdb = UserDatabase(self.env)

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'trac_oidc.login'

    def get_navigation_items(self, req):
        oidc_href = req.href.trac_oidc
        path_qs = req.path_info
        if req.query_string:
            path_qs += '?' + req.query_string

        if not req.authname or req.authname == 'anonymous':
            # Not logged in, show login link
            login_link = tag.a(_('Login using Google'),
                               href=oidc_href('login', return_to=path_qs))
            yield 'metanav', 'trac_oidc.login', login_link

        elif self.show_logout_link:
            # Logged in and LoginModule is disabled, show logout link
            yield ('metanav', 'trac_oidc.login',
                   _('logged in as %(user)s', user=req.authname))
            yield ('metanav', 'trac_oidc.logout',
                   logout_link(oidc_href, return_to=path_qs))

    # IRequestHandler methods

    def match_request(self, req):
        path_info = req.path_info
        if path_info == '/login' and self.show_logout_link:
            # Stock LoginModule is disabled, so handle default /login too
            return True
        return path_info in ('/trac_oidc/login', '/trac_oidc/logout',
                             '/trac_oidc/redirect')

    def process_request(self, req):
        if req.path_info.endswith('/logout'):
            return_url = self._get_return_url(req)
            self._forget_user(req)
            return req.redirect(return_url)
        elif req.path_info.endswith('/login'):
            # Start the login process by redirectory to OP
            req.session[self.RETURN_URL_SKEY] = self._get_return_url(req)
            authenticator = self._get_authenticator(req)
            return req.redirect(authenticator.get_auth_url(req))
        elif req.path_info.endswith('/redirect'):
            # Finish the login process after redirect from OP
            return_url = req.session.pop(self.RETURN_URL_SKEY, req.abs_href())
            id_token = self._retrieve_id(req)
            if id_token:
                authname = self._find_or_create_session(req, id_token)
                assert authname
                self.log.debug("Logging in as %r", authname)
                self._remember_user(req, authname)
            return req.redirect(return_url)

    # private methods

    def _retrieve_id(self, req):
        """ Retrieve oidc id_token from provider.

        Returns ``None`` if authentication was unsuccessful for any reason.

        """
        authenticator = self._get_authenticator(req)
        try:
            return authenticator.get_identity(req)
        except AuthenticationFailed as ex:
            self.log.info("Authentication failed: %s", ex)
            add_warning(req, "Authentication failed: %s", ex)
        except AuthenticationError as ex:
            self.log.error("Authentication error: %s", ex)
            add_warning(req, "Authentication error: %s", ex)

    def _find_or_create_session(self, req, id_token):
        """ Find or create authenticated session for subject.
        """
        userdb = self.userdb
        authname = userdb.find_session(id_token)
        if not authname:
            # There is no authenticated session for the user,
            # create a new one
            # XXX: should it be configurable whether this happens?
            authname = userdb.create_session(id_token)
            add_notice(
                req,
                _(
                    "Hello! You appear to be new here. "
                    "A new authenticated session with "
                    "username '%(authname)s' has been created for you.",
                    authname=authname))
        return authname

    def _remember_user(self, req, authname):
        for lm in self.login_managers:
            lm.remember_user(req, authname)

    def _forget_user(self, req):
        for lm in self.login_managers:
            lm.forget_user(req)

    def _get_authenticator(self, req):
        conf_dir = os.path.join(self.env.path, 'conf')
        client_secret_file = os.path.join(conf_dir, self.client_secret_file)
        redirect_url = req.abs_href.trac_oidc('redirect')
        openid_realm = self._get_openid_realm(req)
        self.log.debug('openid_realm = %r', openid_realm)
        return Authenticator(client_secret_file, redirect_url, openid_realm,
                             self.log)

    def _get_openid_realm(self, req):
        """ Get the OpenID realm.

        This computes the OpenID realm in exactly the same manner
        that the ``TracAuthOpenID`` plugin does.

        Note that I'm not sure this is really the “right” way to do it,
        but, since we want to get back the same identity URLs from google
        as we did using ``TracAuthOpenID``, here we are.

        """
        href = req.href()
        abs_href = self.env.abs_href()
        if href and abs_href.endswith(href):
            base_url = abs_href[:-len(href)]
        else:  # pragma: NO COVER
            base_url = abs_href

        if self.absolute_trust_root:
            path = '/'
        else:
            path = href
        return base_url + path

    @staticmethod
    def _get_return_url(req):
        return_to = req.args.getfirst('return_to', '/')
        # We expect return_to to be a URL relative to the trac's base_path.
        # Be paranoid about this.
        scheme, netloc, path, query, anchor = urlsplit(return_to)
        if scheme or netloc or '..' in path.split('/') or anchor:
            # return url looks suspicious, ignore it.
            return req.abs_href()
        return_url = req.abs_href(path)
        if query:
            return_url += '?' + query
        return return_url
Ejemplo n.º 29
0
 def __init__(self, section, name, interface, default=None, doc=''):
     Option.__init__(self, section, name, default, doc)
     self.xtnpt = ExtensionPoint(interface)
Ejemplo n.º 30
0
 def __init__(self, section, name, interface, default=None,
              include_missing=True, doc='', doc_domain='tracini'):
     ListOption.__init__(self, section, name, default, doc=doc,
                         doc_domain=doc_domain)
     self.xtnpt = ExtensionPoint(interface)
     self.include_missing = include_missing
Ejemplo n.º 31
0
 def __init__(self, section, name, interface, default=None,
              include_missing=True, doc=''):
     ListOption.__init__(self, section, name, default, doc=doc)
     self.xtnpt = ExtensionPoint(interface)
     self.include_missing = include_missing
Ejemplo n.º 32
0
class DownloadsConsoleAdmin(Component):
    """
        The consoleadmin module implements downloads plugin administration
        via trac-admin command.
    """
    implements(IAdminCommandProvider)

    # Download change listeners.
    change_listeners = ExtensionPoint(IDownloadChangeListener)

    # Configuration options.
    consoleadmin_user = Option(
        'downloads',
        'consoleadmin_user',
        'anonymous',
        doc=
        'User whos permissons will be used to upload download. User should have TAGS_MODIFY permissons.'
    )

    # IAdminCommandProvider

    def get_admin_commands(self):
        yield ('download list', '', 'Show uploaded downloads', None,
               self._do_list)
        yield ('download add', '<file> [description=<description>]'
               ' [author=<author>]\n  [tags="<tag1> <tag2> ..."]'
               ' [component=<component>] [version=<version>]\n'
               ' [platform=<platform>]'
               ' [type=<type>]', 'Add new download', None, self._do_add)
        yield ('download remove', '<filename> | <download_id>',
               'Remove uploaded download', None, self._do_remove)

    # Internal methods.

    def _do_list(self):
        # Get downloads API component.
        api = self.env[DownloadsApi]

        # Create context.
        context = Context('downloads-consoleadmin')
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        # Print uploded download
        downloads = api.get_downloads(context)
        print_table(
            [(download['id'], download['file'], pretty_size(
                download['size']), format_datetime(download['time']),
              download['component'], download['version'],
              download['platform']['name'], download['type']['name'])
             for download in downloads], [
                 'ID', 'Filename', 'Size', 'Uploaded', 'Component', 'Version',
                 'Platform', 'Type'
             ])

    def _do_add(self, filename, *arguments):
        # Get downloads API component.
        api = self.env[DownloadsApi]

        # Create context.
        context = Context('downloads-consoleadmin')
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()
        context.req = FakeRequest(self.env, self.consoleadmin_user)

        # Be sure, we have correct path
        req_path = conf.getEnvironmentDownloadsPath(self.env)

        # Convert relative path to absolute.
        if not os.path.isabs(filename):
            filename = os.path.join(req_path, filename)

        # Open file object.
        file, filename, file_size = self._get_file(filename)

        # Create download dictionary from arbitrary attributes.
        download = {
            'file': filename,
            'size': file_size,
            'time': to_timestamp(datetime.now(utc)),
            'count': 0
        }

        # Read optional attributes from arguments.
        for argument in arguments:
            # Check correct format.
            argument = argument.split("=")
            if len(argument) != 2:
                AdminCommandError(
                    _('Invalid format of download attribute:'
                      ' %(value)s',
                      value=argument))
            name, value = argument

            # Check known arguments.
            if not name in ('description', 'author', 'tags', 'component',
                            'version', 'platform', 'type'):
                raise AdminCommandError(
                    _('Invalid download attribute: %(value)s', value=name))

            # Transform platform and type name to ID.
            if name == 'platform':
                value = api.get_platform_by_name(context, value)['id']
            elif name == 'type':
                value = api.get_type_by_name(context, value)['id']

            # Add attribute to download.
            download[name] = value

        self.log.debug(download)

        # Upload file to DB and file storage.
        api.store_download(context, download, file)

        # Close input file and commit changes in DB.
        file.close()
        db.commit()

    def _do_remove(self, identifier):
        # Get downloads API component.
        api = self.env[DownloadsApi]

        # Create context.
        context = Context('downloads-consoleadmin')
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()
        context.req = FakeRequest(self.env, self.consoleadmin_user)

        # Get download by ID or filename.
        try:
            download_id = int(identifier)
            download = api.get_download(context, download_id)
        except ValueError:
            download = api.get_download_by_file(context, identifier)

        # Check if download exists.
        if not download:
            raise AdminCommandError(
                _('Invalid download identifier: %(value)s', value=identifier))

        # Delete download by ID.
        api.remove_download(context, download)

        # Commit changes in DB.
        db.commit()

    def _get_file(self, filename):
        # Open file and get its size
        file = open(filename, 'rb')
        size = os.fstat(file.fileno())[6]

        # Check non-emtpy file.
        if size == 0:
            raise TracError('Can\'t upload empty file.')

        # Try to normalize the filename to unicode NFC if we can.
        # Files uploaded from OS X might be in NFD.
        filename = unicodedata.normalize('NFC', to_unicode(filename, 'utf-8'))
        filename = filename.replace('\\', '/').replace(':', '/')
        filename = os.path.basename(filename)

        return file, filename, size